Database Performance Optimization
This document describes the performance optimizations applied to the Lager Guru database.
Last Updated: 2025-02-03
Overview
Performance optimizations were applied via migration 20250203000000_performance_optimization.sql to improve query performance without breaking existing functionality.
Key Principles:
- ✅ Additive-only changes (indexes, views, functions)
- ✅ No schema changes or data modifications
- ✅ Backward compatible
- ✅ RLS security maintained
Part 1: Missing Indexes
Strategy
Indexes were added on:
- tenant_id columns - Critical for multi-tenant filtering
- Frequently queried columns - status, created_at, assigned_to, etc.
- Composite indexes - For common query patterns (tenant_id + status, tenant_id + created_at)
Indexes Added
Zones
idx_zones_tenant_id- Tenant filteringidx_zones_code- Code lookupsidx_zones_tenant_code- Composite for tenant + code queries
Inventory Items
idx_inventory_items_tenant_id- Tenant filteringidx_inventory_items_tenant_sku- Tenant + SKU lookupsidx_inventory_items_tenant_zone- Tenant + zone filteringidx_inventory_items_tenant_low_stock- Partial index for low stock items
Stock Movements
idx_stock_movements_tenant_id- Tenant filteringidx_stock_movements_tenant_item- Tenant + item queriesidx_stock_movements_tenant_created- Tenant + date sortingidx_stock_movements_tenant_type- Tenant + movement type
Pick Lists
idx_pick_lists_tenant_id- Tenant filteringidx_pick_lists_tenant_status- Tenant + status filteringidx_pick_lists_tenant_created- Tenant + date sortingidx_pick_lists_tenant_assigned- Tenant + assignee filteringidx_pick_lists_tenant_completed- Tenant + completion date
Shipments
idx_shipments_tenant_id- Tenant filteringidx_shipments_tenant_status- Tenant + status filteringidx_shipments_tenant_created- Tenant + date sortingidx_shipments_tenant_driver- Tenant + driver filteringidx_shipments_tenant_zones- Tenant + zone filtering
Safety Tables
idx_safety_incidents_tenant_id- Tenant filteringidx_safety_incidents_tenant_created- Tenant + date sortingidx_safety_incidents_tenant_category- Tenant + category filteringidx_safety_incidents_tenant_severity- Tenant + severity filteringidx_safety_actions_tenant_id- Tenant filteringidx_safety_actions_tenant_assigned- Tenant + assignee filteringidx_safety_actions_tenant_status- Tenant + status filteringidx_safety_actions_tenant_due_date- Tenant + due date filtering
RLS Performance
idx_tenant_users_user_id- User lookupsidx_tenant_users_tenant_id- Tenant lookupsidx_tenant_users_user_tenant- Composite for user + tenantidx_user_roles_user_id- User role lookupsidx_user_roles_role- Role filteringidx_user_roles_user_role- Composite for user + role
Index Creation Method
Indexes are created using CREATE INDEX IF NOT EXISTS to avoid errors if they already exist.
Note: For production databases with large tables, consider creating indexes during a maintenance window as they may briefly lock tables. For very large tables, you can create indexes manually using CREATE INDEX CONCURRENTLY outside of a transaction block.
Part 2: RLS Policy Optimization
Strategy
RLS policies were optimized to:
- Reduce nested EXISTS queries - Use helper functions instead
- Reuse tenant_id filtering - Cache tenant IDs in helper functions
- Minimize per-row execution cost - Use STABLE functions
Helper Function
get_user_tenant_ids() - Returns all tenant IDs for the current user
- Uses
SECURITY DEFINERto bypass RLS recursion - Marked as
STABLEfor query planner optimization - Cached by PostgreSQL for the duration of the query
Policy Pattern
Before:
USING (
EXISTS (
SELECT 1 FROM public.tenant_users tu
WHERE tu.tenant_id = table.tenant_id
AND tu.user_id = auth.uid()
)
)After:
USING (
tenant_id IN (SELECT tenant_id FROM public.get_user_tenant_ids())
OR public.is_admin()
)This reduces the number of EXISTS subqueries executed per row.
Part 3: Read-Optimized Views
Strategy
Created materialized views for common dashboard queries to:
- Pre-join related tables - Reduce join overhead
- Pre-calculate aggregations - Item counts, overdue flags, etc.
- Respect RLS - Views use helper functions for tenant filtering
Views Created
Admin Dashboard Views
admin_dashboard_zones
- Zones with utilization data pre-joined
- Includes utilization_percentage and active_shipments
- Respects tenant isolation
admin_dashboard_inventory
- Inventory items with zone info pre-joined
- Includes low_stock flag (calculated)
- Respects tenant isolation
admin_dashboard_pick_lists
- Active pick lists with item count pre-calculated
- Filters to pending/in_progress status
- Respects tenant isolation
Safety Dashboard Views
safety_dashboard_incidents
- Recent incidents with reporter and zone info pre-joined
- Limited to 100 most recent
- Respects tenant isolation and role-based access
safety_dashboard_actions
- Open actions with assignee info pre-joined
- Includes overdue flag (calculated)
- Respects tenant isolation and role-based access
Worker Dashboard Views
worker_dashboard_pick_lists
- Assigned pick lists with progress tracking
- Includes items_picked and items_pending counts
- Filters to pending/picking status
- Respects tenant isolation and assignment
View Usage
Views can be queried directly:
SELECT * FROM public.admin_dashboard_zones;
SELECT * FROM public.safety_dashboard_incidents;
SELECT * FROM public.worker_dashboard_pick_lists;RLS policies are automatically applied through the helper functions used in the view definitions.
Part 4: Query Planner Optimization
ANALYZE Commands
The migration runs ANALYZE on all major tables to update statistics for the query planner. This helps PostgreSQL:
- Choose optimal query plans
- Use indexes effectively
- Estimate row counts accurately
Note: ANALYZE should be run periodically (weekly/monthly) or after large data changes.
Part 5: Performance Monitoring
Helper Functions
check_index_usage()
- Returns index usage statistics
- Helps identify unused indexes that can be dropped
- Low
idx_scanvalues indicate unused indexes
check_table_sizes()
- Returns table size statistics
- Helps identify large tables that may need partitioning
- Shows row counts, table size, and index size
Usage
-- Check index usage
SELECT * FROM public.check_index_usage()
WHERE idx_scan < 10
ORDER BY tablename, indexname;
-- Check table sizes
SELECT * FROM public.check_table_sizes()
ORDER BY row_count DESC;Performance Improvements
Expected Improvements
- Dashboard Queries: 50-70% faster due to indexes and views
- RLS Policy Evaluation: 30-50% faster due to reduced nested EXISTS
- Multi-tenant Filtering: 60-80% faster due to tenant_id indexes
- Join Operations: 40-60% faster due to composite indexes
Monitoring
Monitor query performance using:
- Supabase Dashboard → Database → Query Performance
- PostgreSQL
pg_stat_statementsextension - Application-level query timing
Maintenance
Regular Tasks
- Weekly: Run
ANALYZEon frequently updated tables - Monthly: Review index usage with
check_index_usage() - Quarterly: Review table sizes with
check_table_sizes() - As Needed: Drop unused indexes identified by monitoring
Index Maintenance
-- Rebuild indexes if needed
REINDEX INDEX CONCURRENTLY idx_zones_tenant_id;
-- Update statistics
ANALYZE public.zones;Backward Compatibility
✅ All changes are backward compatible:
- No schema changes
- No data modifications
- No API changes required
- Existing queries continue to work
- Views are optional (can still query tables directly)
Rollback
If needed, rollback can be performed by:
- Dropping views:
DROP VIEW IF EXISTS public.admin_dashboard_zones CASCADE; - Dropping indexes:
DROP INDEX CONCURRENTLY IF EXISTS idx_zones_tenant_id; - Dropping functions:
DROP FUNCTION IF EXISTS public.get_user_tenant_ids();
Note: Index drops using CONCURRENTLY require a separate transaction.