Database Performance Optimization
Database optimization strategies including query optimization, indexing, and monitoring for Smart Shelf.
Database Performance Optimization
Database performance is fundamental to Smart Shelf's overall responsiveness. This section covers comprehensive strategies for optimizing PostgreSQL database performance, from query optimization to monitoring and maintenance.
Query Optimization
Index Strategy
Proper indexing is crucial for query performance, especially with large datasets.
Primary Indexes for Core Operations
-- Products table indexes
CREATE INDEX CONCURRENTLY idx_products_name ON products(name);
CREATE INDEX CONCURRENTLY idx_products_sku ON products(sku);
CREATE INDEX CONCURRENTLY idx_products_category ON products(category_id);
CREATE INDEX CONCURRENTLY idx_products_active ON products(is_active) WHERE is_active = true;
CREATE INDEX CONCURRENTLY idx_products_created_at ON products(created_at DESC);
-- Inventory table indexes
CREATE INDEX CONCURRENTLY idx_inventory_product_warehouse
ON inventory(product_id, warehouse_id);
CREATE INDEX CONCURRENTLY idx_inventory_quantity ON inventory(quantity_on_hand);
CREATE INDEX CONCURRENTLY idx_inventory_low_stock
ON inventory(product_id) WHERE quantity_on_hand <= reorder_point;
-- Stock movements table indexes
CREATE INDEX CONCURRENTLY idx_stock_movements_product_date
ON stock_movements(product_id, created_at DESC);
CREATE INDEX CONCURRENTLY idx_stock_movements_type ON stock_movements(movement_type);
CREATE INDEX CONCURRENTLY idx_stock_movements_warehouse ON stock_movements(warehouse_id);
-- Orders table indexes
CREATE INDEX CONCURRENTLY idx_purchase_orders_supplier ON purchase_orders(supplier_id);
CREATE INDEX CONCURRENTLY idx_purchase_orders_status ON purchase_orders(status);
CREATE INDEX CONCURRENTLY idx_purchase_orders_date ON purchase_orders(created_at DESC);
CREATE INDEX CONCURRENTLY idx_sales_orders_customer ON sales_orders(customer_id);
CREATE INDEX CONCURRENTLY idx_sales_orders_status ON sales_orders(status);
CREATE INDEX CONCURRENTLY idx_sales_orders_date ON sales_orders(created_at DESC);
Composite Indexes for Multi-Column Queries
-- Frequently used filter combinations
CREATE INDEX CONCURRENTLY idx_products_category_active
ON products(category_id, is_active) WHERE is_active = true;
CREATE INDEX CONCURRENTLY idx_inventory_warehouse_available
ON inventory(warehouse_id, quantity_on_hand) WHERE quantity_on_hand > 0;
CREATE INDEX CONCURRENTLY idx_orders_status_date
ON purchase_orders(status, created_at DESC);
-- Search and filter combinations
CREATE INDEX CONCURRENTLY idx_products_search_category
ON products(category_id, name) WHERE is_active = true;
Partial Indexes for Specific Conditions
-- Index only active products
CREATE INDEX CONCURRENTLY idx_products_active_name
ON products(name) WHERE is_active = true;
-- Index only available inventory
CREATE INDEX CONCURRENTLY idx_inventory_available
ON inventory(product_id, warehouse_id) WHERE quantity_on_hand > 0;
-- Index only pending orders
CREATE INDEX CONCURRENTLY idx_orders_pending
ON purchase_orders(created_at DESC) WHERE status = 'pending';
-- Index only recent stock movements
CREATE INDEX CONCURRENTLY idx_stock_movements_recent
ON stock_movements(product_id, created_at DESC)
WHERE created_at > CURRENT_DATE - INTERVAL '30 days';
Full-Text Search Indexes
-- Full-text search for products
ALTER TABLE products ADD COLUMN search_vector tsvector;
UPDATE products SET search_vector =
to_tsvector('english', name || ' ' || coalesce(description, '') || ' ' || sku);
CREATE INDEX CONCURRENTLY idx_products_search
ON products USING gin(search_vector);
-- Trigger to maintain search vector
CREATE OR REPLACE FUNCTION update_product_search_vector()
RETURNS trigger AS $$
BEGIN
NEW.search_vector := to_tsvector('english',
NEW.name || ' ' || coalesce(NEW.description, '') || ' ' || NEW.sku
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER products_search_vector_update
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION update_product_search_vector();
Query Optimization Techniques
Write efficient queries that leverage indexes and minimize resource usage.
Optimized Product Search Query
-- Before optimization (slow)
SELECT
p.id,
p.name,
p.sku,
p.price,
c.name as category_name,
i.quantity_on_hand,
w.name as warehouse_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN inventory i ON p.id = i.product_id
LEFT JOIN warehouses w ON i.warehouse_id = w.id
WHERE p.name ILIKE '%widget%'
ORDER BY p.name
LIMIT 50;
-- After optimization (fast)
SELECT
p.id,
p.name,
p.sku,
p.price,
c.name as category_name,
i.quantity_on_hand,
w.name as warehouse_name
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN inventory i ON p.id = i.product_id AND i.quantity_on_hand > 0
JOIN warehouses w ON i.warehouse_id = w.id
WHERE p.is_active = true
AND p.search_vector @@ plainto_tsquery('english', 'widget')
ORDER BY p.name
LIMIT 50;
Efficient Inventory Summary Query
-- Optimized inventory summary with aggregation
WITH inventory_summary AS (
SELECT
p.id,
p.name,
p.sku,
SUM(i.quantity_on_hand) as total_quantity,
COUNT(i.warehouse_id) as warehouse_count,
AVG(i.quantity_on_hand) as avg_quantity_per_warehouse
FROM products p
JOIN inventory i ON p.id = i.product_id
WHERE p.is_active = true
AND i.quantity_on_hand >= 0
GROUP BY p.id, p.name, p.sku
HAVING SUM(i.quantity_on_hand) > 0
)
SELECT * FROM inventory_summary
ORDER BY total_quantity DESC
LIMIT 100;
Optimized Recent Activity Query
-- Recent stock movements with efficient date filtering
SELECT
sm.id,
sm.movement_type,
sm.quantity,
sm.created_at,
p.name as product_name,
p.sku,
w.name as warehouse_name
FROM stock_movements sm
JOIN products p ON sm.product_id = p.id
JOIN warehouses w ON sm.warehouse_id = w.id
WHERE sm.created_at >= CURRENT_DATE - INTERVAL '7 days'
AND sm.created_at <= CURRENT_DATE + INTERVAL '1 day'
ORDER BY sm.created_at DESC
LIMIT 100;
Query Analysis and Monitoring
Use PostgreSQL's EXPLAIN functionality to analyze and optimize queries.
Query Performance Analysis
-- Analyze query execution plan
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT
p.id,
p.name,
i.quantity_on_hand,
w.name as warehouse_name
FROM products p
JOIN inventory i ON p.id = i.product_id
JOIN warehouses w ON i.warehouse_id = w.id
WHERE p.category_id = '123e4567-e89b-12d3-a456-426614174000'
AND i.quantity_on_hand > 0
ORDER BY p.name
LIMIT 50;
-- Check for sequential scans (should be avoided on large tables)
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch,
seq_tup_read / GREATEST(seq_scan, 1) as avg_seq_read
FROM pg_stat_user_tables
WHERE seq_scan > 1000 -- Tables with many sequential scans
ORDER BY seq_tup_read DESC;
-- Identify slow queries
SELECT
query,
calls,
total_time,
mean_time,
rows,
100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements
WHERE mean_time > 100 -- Queries taking more than 100ms on average
ORDER BY total_time DESC
LIMIT 20;
Connection Management
Connection Pooling
Implement efficient connection pooling to handle concurrent requests.
Supabase Connection Configuration
// lib/supabase/connection-pool.ts
import { createClient } from '@supabase/supabase-js'
interface PoolConfig {
min: number
max: number
idleTimeoutMillis: number
connectionTimeoutMillis: number
}
class ConnectionPool {
private pools: Map<string, any> = new Map()
private config: PoolConfig
constructor(config: PoolConfig) {
this.config = config
}
getClient(key: string = 'default') {
if (!this.pools.has(key)) {
const client = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
db: {
schema: 'public',
},
auth: {
autoRefreshToken: false,
persistSession: false,
},
global: {
headers: {
'X-Client-Info': 'smart-shelf@1.0.0',
},
},
}
)
this.pools.set(key, client)
}
return this.pools.get(key)
}
async executeQuery<T>(
queryFn: (client: any) => Promise<T>,
poolKey: string = 'default'
): Promise<T> {
const client = this.getClient(poolKey)
const startTime = performance.now()
try {
const result = await queryFn(client)
const duration = performance.now() - startTime
// Log slow queries
if (duration > 1000) {
console.warn(`Slow query detected: ${duration}ms`)
}
return result
} catch (error) {
console.error('Database query error:', error)
throw error
}
}
}
export const connectionPool = new ConnectionPool({
min: 2,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000
})
Connection Monitoring
-- Monitor active connections
SELECT
state,
count(*) as connections,
max(now() - state_change) as max_duration
FROM pg_stat_activity
WHERE state IS NOT NULL
GROUP BY state
ORDER BY connections DESC;
-- Monitor connection usage by database
SELECT
datname,
count(*) as connections,
max(now() - state_change) as max_duration
FROM pg_stat_activity
WHERE state = 'active'
GROUP BY datname
ORDER BY connections DESC;
-- Check for long-running queries
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE state = 'active'
AND now() - pg_stat_activity.query_start > interval '5 minutes'
ORDER BY duration DESC;
Database Monitoring
Performance Metrics Collection
Monitor key database performance metrics for optimization opportunities.
Database Performance Monitoring Queries
-- Table sizes and growth
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
pg_total_relation_size(schemaname||'.'||tablename) as size_bytes
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
-- Index usage statistics
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexname::regclass)) as index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
-- Unused indexes (candidates for removal)
SELECT
schemaname,
tablename,
indexname,
idx_scan,
pg_size_pretty(pg_relation_size(indexname::regclass)) as index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND indexname NOT LIKE '%_pkey' -- Exclude primary keys
ORDER BY pg_relation_size(indexname::regclass) DESC;
-- Cache hit ratios
SELECT
'Database' as type,
round(100.0 * sum(blks_hit) / (sum(blks_hit) + sum(blks_read)), 2) as hit_ratio
FROM pg_stat_database
WHERE datname = current_database()
UNION ALL
SELECT
'Tables' as type,
round(100.0 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)), 2) as hit_ratio
FROM pg_statio_user_tables
UNION ALL
SELECT
'Indexes' as type,
round(100.0 * sum(idx_blks_hit) / (sum(idx_blks_hit) + sum(idx_blks_read)), 2) as hit_ratio
FROM pg_statio_user_indexes;
Automated Performance Monitoring
Implement automated monitoring for proactive performance management.
Performance Monitoring Script
// lib/monitoring/database-monitoring.ts
interface DatabaseMetrics {
connectionCount: number
activeQueries: number
cacheHitRatio: number
avgQueryTime: number
slowQueries: number
tableSize: Record<string, number>
indexUsage: Record<string, number>
}
export class DatabaseMonitor {
private supabase: any
constructor(supabase: any) {
this.supabase = supabase
}
async collectMetrics(): Promise<DatabaseMetrics> {
const [
connections,
queries,
cacheHit,
slowQueries,
tableSizes,
indexUsage
] = await Promise.all([
this.getConnectionCount(),
this.getActiveQueries(),
this.getCacheHitRatio(),
this.getSlowQueries(),
this.getTableSizes(),
this.getIndexUsage()
])
return {
connectionCount: connections,
activeQueries: queries.length,
cacheHitRatio: cacheHit,
avgQueryTime: queries.length > 0
? queries.reduce((sum, q) => sum + q.duration, 0) / queries.length
: 0,
slowQueries: slowQueries.length,
tableSize: tableSizes,
indexUsage: indexUsage
}
}
private async getConnectionCount(): Promise<number> {
const { data } = await this.supabase.rpc('get_connection_count')
return data || 0
}
private async getActiveQueries(): Promise<any[]> {
const { data } = await this.supabase.rpc('get_active_queries')
return data || []
}
private async getCacheHitRatio(): Promise<number> {
const { data } = await this.supabase.rpc('get_cache_hit_ratio')
return data || 0
}
private async getSlowQueries(): Promise<any[]> {
const { data } = await this.supabase.rpc('get_slow_queries')
return data || []
}
private async getTableSizes(): Promise<Record<string, number>> {
const { data } = await this.supabase.rpc('get_table_sizes')
return data?.reduce((acc: any, row: any) => {
acc[row.table_name] = row.size_bytes
return acc
}, {}) || {}
}
private async getIndexUsage(): Promise<Record<string, number>> {
const { data } = await this.supabase.rpc('get_index_usage')
return data?.reduce((acc: any, row: any) => {
acc[row.index_name] = row.usage_count
return acc
}, {}) || {}
}
async generateReport(): Promise<string> {
const metrics = await this.collectMetrics()
return `
Database Performance Report
===========================
Connections: ${metrics.connectionCount}
Active Queries: ${metrics.activeQueries}
Cache Hit Ratio: ${metrics.cacheHitRatio.toFixed(2)}%
Average Query Time: ${metrics.avgQueryTime.toFixed(2)}ms
Slow Queries: ${metrics.slowQueries}
Largest Tables:
${Object.entries(metrics.tableSize)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([table, size]) => ` ${table}: ${(size / 1024 / 1024).toFixed(2)} MB`)
.join('\n')}
Most Used Indexes:
${Object.entries(metrics.indexUsage)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([index, usage]) => ` ${index}: ${usage} scans`)
.join('\n')}
`.trim()
}
}
// Usage
export async function monitorDatabase() {
const monitor = new DatabaseMonitor(connectionPool.getClient())
const report = await monitor.generateReport()
console.log(report)
// Send to monitoring service or save to logs
}
Database Health Checks
Implement regular health checks to identify and address issues proactively.
Health Check Functions
-- Create database functions for health monitoring
CREATE OR REPLACE FUNCTION get_connection_count()
RETURNS INTEGER AS $$
BEGIN
RETURN (SELECT count(*) FROM pg_stat_activity WHERE state = 'active');
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION get_cache_hit_ratio()
RETURNS NUMERIC AS $$
BEGIN
RETURN (
SELECT round(
100.0 * sum(blks_hit) / (sum(blks_hit) + sum(blks_read)), 2
)
FROM pg_stat_database
WHERE datname = current_database()
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION get_slow_queries()
RETURNS TABLE(
query_text TEXT,
duration_seconds NUMERIC,
calls BIGINT,
mean_time NUMERIC
) AS $$
BEGIN
RETURN QUERY
SELECT
query,
EXTRACT(EPOCH FROM (total_time / 1000.0)) as duration_seconds,
calls,
mean_time
FROM pg_stat_statements
WHERE mean_time > 100 -- Queries taking more than 100ms
ORDER BY total_time DESC
LIMIT 10;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION get_table_sizes()
RETURNS TABLE(
table_name TEXT,
size_bytes BIGINT
) AS $$
BEGIN
RETURN QUERY
SELECT
tablename::TEXT,
pg_total_relation_size(schemaname||'.'||tablename) as size_bytes
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Automated Health Check Service
// lib/services/health-check.ts
export class DatabaseHealthCheck {
private supabase: any
private alertThresholds = {
maxConnections: 80,
minCacheHitRatio: 95,
maxSlowQueries: 5,
maxAvgQueryTime: 200
}
constructor(supabase: any) {
this.supabase = supabase
}
async performHealthCheck(): Promise<{
healthy: boolean
issues: string[]
metrics: any
}> {
const monitor = new DatabaseMonitor(this.supabase)
const metrics = await monitor.collectMetrics()
const issues: string[] = []
// Check connection count
if (metrics.connectionCount > this.alertThresholds.maxConnections) {
issues.push(`High connection count: ${metrics.connectionCount}`)
}
// Check cache hit ratio
if (metrics.cacheHitRatio < this.alertThresholds.minCacheHitRatio) {
issues.push(`Low cache hit ratio: ${metrics.cacheHitRatio.toFixed(2)}%`)
}
// Check slow queries
if (metrics.slowQueries > this.alertThresholds.maxSlowQueries) {
issues.push(`Too many slow queries: ${metrics.slowQueries}`)
}
// Check average query time
if (metrics.avgQueryTime > this.alertThresholds.maxAvgQueryTime) {
issues.push(`High average query time: ${metrics.avgQueryTime.toFixed(2)}ms`)
}
return {
healthy: issues.length === 0,
issues,
metrics
}
}
async scheduleHealthChecks() {
// Run health check every 5 minutes
setInterval(async () => {
try {
const result = await this.performHealthCheck()
if (!result.healthy) {
console.error('Database health check failed:', result.issues)
// Send alerts to monitoring service
} else {
console.log('Database health check passed')
}
} catch (error) {
console.error('Health check error:', error)
}
}, 5 * 60 * 1000) // 5 minutes
}
}
Database performance optimization is an ongoing process that requires regular monitoring, analysis, and tuning. These strategies provide a comprehensive foundation for maintaining optimal database performance in Smart Shelf.