CrackedRuby logo

CrackedRuby

Database Optimization

A comprehensive guide to optimizing database operations, query performance, and connection management in Ruby applications.

Overview

Database optimization in Ruby involves improving query performance, managing connections efficiently, and reducing database load through strategic caching and query design. Ruby provides several libraries and techniques for database optimization, with ActiveRecord being the most common ORM for Rails applications, while libraries like Sequel and ROM offer alternative approaches.

The primary components of database optimization in Ruby include query optimization through proper indexing and query structure, connection pooling to manage database connections efficiently, and strategic use of eager loading to reduce N+1 query problems. Ruby's database optimization also encompasses caching strategies, background job processing for expensive operations, and monitoring tools to identify performance bottlenecks.

# Basic query optimization with eager loading
User.includes(:posts, :comments).where(active: true).limit(100)

# Connection pooling configuration
ActiveRecord::Base.establish_connection(
  adapter: 'postgresql',
  pool: 25,
  timeout: 5000,
  database: 'myapp_production'
)

# Query caching example
Rails.cache.fetch("user_stats_#{user.id}", expires_in: 1.hour) do
  user.posts.joins(:comments).group(:category).count
end

Ruby's approach to database optimization emphasizes declarative query building, intelligent caching mechanisms, and comprehensive monitoring capabilities. The ecosystem provides tools for query analysis, performance monitoring, and automated optimization suggestions through gems like bullet, rack-mini-profiler, and pg_query.

Basic Usage

Database optimization in Ruby typically starts with proper query structure and eager loading to eliminate N+1 queries. The includes method loads associated records in a single query rather than executing separate queries for each association.

# N+1 query problem - inefficient
users = User.all
users.each do |user|
  puts user.posts.count  # Triggers separate query for each user
end

# Optimized with eager loading
users = User.includes(:posts)
users.each do |user|
  puts user.posts.size  # Uses preloaded data
end

Connection pooling manages database connections efficiently by reusing existing connections rather than creating new ones for each request. Ruby's connection pool automatically handles connection lifecycle and prevents connection exhaustion.

# Configure connection pool
ActiveRecord::Base.configurations['production'] = {
  'adapter' => 'postgresql',
  'database' => 'myapp_production',
  'pool' => 20,
  'timeout' => 5000,
  'checkout_timeout' => 5,
  'reaping_frequency' => 10
}

# Monitor connection usage
pool = ActiveRecord::Base.connection_pool
puts "Connections in use: #{pool.connections.count}"
puts "Available connections: #{pool.available_connection}"

Query optimization involves selecting only necessary columns, using appropriate indexes, and structuring queries to minimize database work. The select method limits returned columns, while proper where clauses leverage database indexes.

# Select specific columns
User.select(:id, :email, :name).where(active: true)

# Use indexes effectively
User.where(email: 'user@example.com').first  # Assumes email is indexed

# Batch processing for large datasets
User.find_each(batch_size: 1000) do |user|
  user.update_last_login
end

Caching strategies reduce database load by storing frequently accessed data in memory. Ruby supports multiple caching approaches including query result caching, fragment caching, and key-value caching.

# Query result caching
cached_users = Rails.cache.fetch('active_users', expires_in: 30.minutes) do
  User.where(active: true).to_a
end

# Fragment caching with cache keys
cache_key = "user_dashboard_#{user.id}_#{user.updated_at.to_i}"
Rails.cache.fetch(cache_key) do
  expensive_dashboard_calculation(user)
end

Advanced Usage

Advanced database optimization in Ruby involves sophisticated query patterns, custom SQL integration, and performance monitoring. Complex queries often require raw SQL or advanced ActiveRecord methods to achieve optimal performance.

# Custom SQL for complex aggregations
result = ActiveRecord::Base.connection.execute(<<~SQL)
  SELECT 
    DATE_TRUNC('month', created_at) as month,
    COUNT(*) as total_users,
    COUNT(CASE WHEN plan = 'premium' THEN 1 END) as premium_users
  FROM users 
  WHERE created_at >= NOW() - INTERVAL '12 months'
  GROUP BY DATE_TRUNC('month', created_at)
  ORDER BY month DESC
SQL

# Advanced join optimization
User.joins(<<~SQL)
  INNER JOIN (
    SELECT user_id, MAX(created_at) as latest_post
    FROM posts 
    GROUP BY user_id
  ) latest_posts ON users.id = latest_posts.user_id
SQL
.where('latest_posts.latest_post > ?', 1.week.ago)

Database partitioning and sharding strategies distribute data across multiple database instances or tables to improve performance. Ruby applications can implement read/write splitting and horizontal partitioning.

# Read/write splitting configuration
class ApplicationRecord < ActiveRecord::Base
  connects_to database: { 
    writing: :primary, 
    reading: :replica 
  }
end

# Horizontal partitioning by date
class TimePartitionedModel < ApplicationRecord
  def self.partition_table_name(date)
    "#{table_name}_#{date.strftime('%Y_%m')}"
  end
  
  def self.create_partition(date)
    table_name = partition_table_name(date)
    connection.execute(<<~SQL)
      CREATE TABLE IF NOT EXISTS #{table_name} 
      PARTITION OF #{self.table_name}
      FOR VALUES FROM ('#{date.beginning_of_month}') 
      TO ('#{date.end_of_month}')
    SQL
  end
end

Background job processing moves expensive database operations out of the request cycle, improving response times and user experience. Ruby's job processing libraries integrate with database optimization strategies.

# Background job for expensive operations
class DatabaseMaintenanceJob < ApplicationJob
  def perform
    # Update denormalized counters
    User.find_each do |user|
      user.update_column(:posts_count, user.posts.count)
    end
    
    # Clean up old records
    OldRecord.where('created_at < ?', 6.months.ago).delete_all
    
    # Refresh materialized views
    ActiveRecord::Base.connection.execute('REFRESH MATERIALIZED VIEW user_stats')
  end
end

# Delayed computation pattern
class ExpensiveCalculation
  def self.compute_async(user_id)
    CalculationJob.perform_later(user_id)
  end
  
  def self.get_result(user_id)
    Rails.cache.fetch("calculation_#{user_id}") do
      # Return cached result or trigger async calculation
      CalculationJob.perform_later(user_id)
      { status: 'computing', estimated_completion: 5.minutes.from_now }
    end
  end
end

Query optimization through database-specific features leverages PostgreSQL's advanced capabilities like window functions, common table expressions, and custom indexes.

# Window functions for ranking
ranked_users = User.select(<<~SQL)
  *,
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as salary_rank,
  DENSE_RANK() OVER (ORDER BY created_at DESC) as signup_rank
SQL

# Common table expressions for recursive queries
hierarchy = ActiveRecord::Base.connection.execute(<<~SQL)
  WITH RECURSIVE user_hierarchy AS (
    SELECT id, name, manager_id, 1 as level
    FROM users 
    WHERE manager_id IS NULL
    
    UNION ALL
    
    SELECT u.id, u.name, u.manager_id, uh.level + 1
    FROM users u
    INNER JOIN user_hierarchy uh ON u.manager_id = uh.id
  )
  SELECT * FROM user_hierarchy ORDER BY level, name
SQL

Error Handling & Debugging

Database optimization requires robust error handling to manage connection failures, query timeouts, and resource constraints. Ruby provides multiple mechanisms for handling database errors gracefully and implementing retry logic.

# Connection timeout handling
begin
  User.transaction do
    expensive_database_operation
  end
rescue ActiveRecord::ConnectionTimeoutError => e
  Rails.logger.error "Database connection timeout: #{e.message}"
  # Implement exponential backoff
  sleep(2 ** retry_count)
  retry if (retry_count += 1) < 3
rescue ActiveRecord::StatementInvalid => e
  if e.message.include?('deadlock')
    Rails.logger.warn "Deadlock detected, retrying transaction"
    sleep(rand(0.1..0.5))  # Random delay to avoid thundering herd
    retry if (retry_count += 1) < 5
  else
    raise
  end
end

Query debugging involves identifying slow queries, analyzing execution plans, and monitoring database performance metrics. Ruby applications can implement comprehensive logging and monitoring.

# Custom query logging and analysis
class QueryAnalyzer
  def self.log_slow_queries
    original_execute = ActiveRecord::Base.connection.method(:execute)
    
    ActiveRecord::Base.connection.define_singleton_method(:execute) do |sql, name = nil|
      start_time = Time.current
      result = original_execute.call(sql, name)
      duration = Time.current - start_time
      
      if duration > 0.5  # Log queries taking longer than 500ms
        Rails.logger.warn "Slow query (#{duration}s): #{sql}"
        
        # Get execution plan for analysis
        if sql.strip.upcase.start_with?('SELECT')
          plan = execute("EXPLAIN ANALYZE #{sql}")
          Rails.logger.info "Execution plan: #{plan.to_a}"
        end
      end
      
      result
    end
  end
end

# Connection pool monitoring
class ConnectionPoolMonitor
  def self.check_pool_health
    pool = ActiveRecord::Base.connection_pool
    
    {
      size: pool.size,
      checked_out: pool.connections.count(&:in_use?),
      available: pool.available_connection_count,
      dead_connections: pool.connections.count { |conn| !conn.active? }
    }
  end
  
  def self.alert_if_exhausted
    stats = check_pool_health
    if stats[:available] < 2
      Rails.logger.error "Connection pool nearly exhausted: #{stats}"
      # Send alert to monitoring system
      send_alert("Database connection pool exhaustion warning", stats)
    end
  end
end

Memory leak detection and prevention prevents applications from consuming excessive memory due to inefficient database usage patterns. Ruby provides tools for monitoring object allocation and garbage collection.

# Memory monitoring for database operations
class MemoryProfiler
  def self.profile_database_operation(&block)
    GC.start  # Start with clean slate
    memory_before = get_memory_usage
    objects_before = ObjectSpace.count_objects
    
    result = yield
    
    GC.start
    memory_after = get_memory_usage
    objects_after = ObjectSpace.count_objects
    
    {
      result: result,
      memory_diff: memory_after - memory_before,
      objects_created: objects_after[:TOTAL] - objects_before[:TOTAL],
      gc_count: GC.count
    }
  end
  
  private
  
  def self.get_memory_usage
    `ps -o rss= -p #{Process.pid}`.to_i * 1024  # Convert KB to bytes
  end
end

# Usage example
profile = MemoryProfiler.profile_database_operation do
  User.includes(:posts, :comments).limit(1000).map(&:email)
end

Rails.logger.info "Operation used #{profile[:memory_diff]} bytes, created #{profile[:objects_created]} objects"

Deadlock detection and resolution involves implementing retry logic with exponential backoff and designing transactions to minimize lock contention.

# Deadlock-resistant transaction pattern
class DeadlockResistantOperation
  MAX_RETRIES = 5
  BASE_DELAY = 0.1
  
  def self.execute(&block)
    retries = 0
    
    begin
      ActiveRecord::Base.transaction(isolation: :read_committed) do
        yield
      end
    rescue ActiveRecord::Deadlocked => e
      retries += 1
      
      if retries <= MAX_RETRIES
        delay = BASE_DELAY * (2 ** retries) + rand(0.1)
        Rails.logger.warn "Deadlock detected (attempt #{retries}/#{MAX_RETRIES}), retrying in #{delay}s"
        sleep(delay)
        retry
      else
        Rails.logger.error "Max deadlock retries exceeded: #{e.message}"
        raise
      end
    end
  end
end

Performance & Memory

Performance optimization in Ruby database applications focuses on query efficiency, memory management, and resource utilization. Measuring and monitoring performance provides the foundation for optimization decisions.

# Comprehensive performance monitoring
class DatabasePerformanceMonitor
  def self.benchmark_query(description, &block)
    gc_before = GC.stat
    memory_before = get_process_memory
    
    result = nil
    time = Benchmark.realtime do
      result = yield
    end
    
    gc_after = GC.stat
    memory_after = get_process_memory
    
    metrics = {
      description: description,
      execution_time: time,
      memory_used: memory_after - memory_before,
      gc_runs: gc_after[:count] - gc_before[:count],
      objects_allocated: gc_after[:total_allocated_objects] - gc_before[:total_allocated_objects]
    }
    
    log_performance_metrics(metrics)
    result
  end
  
  private
  
  def self.get_process_memory
    `ps -o rss= -p #{Process.pid}`.to_i * 1024
  end
  
  def self.log_performance_metrics(metrics)
    Rails.logger.info "Performance: #{metrics[:description]} - " \
                     "Time: #{metrics[:execution_time].round(3)}s, " \
                     "Memory: #{(metrics[:memory_used] / 1024.0 / 1024.0).round(2)}MB, " \
                     "GC runs: #{metrics[:gc_runs]}"
  end
end

Query optimization strategies include proper indexing, query structure analysis, and batch processing for large datasets. Ruby applications can implement sophisticated caching and pre-computation strategies.

# Batch processing with memory management
class BatchProcessor
  def self.process_large_dataset(model_class, batch_size: 1000)
    total_processed = 0
    
    model_class.find_in_batches(batch_size: batch_size) do |batch|
      ActiveRecord::Base.transaction do
        batch.each do |record|
          yield record
          total_processed += 1
        end
      end
      
      # Force garbage collection every 10 batches
      if (total_processed / batch_size) % 10 == 0
        GC.start
        Rails.logger.info "Processed #{total_processed} records, forced GC"
      end
    end
    
    total_processed
  end
end

# Usage with memory monitoring
processed = BatchProcessor.process_large_dataset(User, batch_size: 500) do |user|
  # Expensive operation on each user
  user.calculate_lifetime_value
  user.update_recommendations
end

Connection pooling optimization involves tuning pool sizes, timeout values, and connection lifecycle management based on application load patterns.

# Dynamic connection pool management
class ConnectionPoolManager
  def self.configure_for_load(concurrent_requests)
    # Calculate optimal pool size based on request concurrency
    pool_size = [concurrent_requests * 1.2, 50].min.to_i
    checkout_timeout = [5 + (concurrent_requests * 0.1), 30].min
    
    config = ActiveRecord::Base.configurations['production'].dup
    config['pool'] = pool_size
    config['checkout_timeout'] = checkout_timeout
    config['reaping_frequency'] = pool_size > 30 ? 5 : 10
    
    ActiveRecord::Base.establish_connection(config)
    
    Rails.logger.info "Configured connection pool: size=#{pool_size}, " \
                     "timeout=#{checkout_timeout}s"
  end
  
  def self.monitor_and_adjust
    stats = ActiveRecord::Base.connection_pool.stat
    
    # Automatic adjustment based on usage patterns
    if stats[:busy] > stats[:size] * 0.8
      increase_pool_size
    elsif stats[:busy] < stats[:size] * 0.3 && stats[:size] > 10
      decrease_pool_size
    end
  end
end

Memory optimization strategies include object pooling, lazy loading, and efficient data structure usage to minimize garbage collection overhead.

# Object pooling for frequently created objects
class QueryResultPool
  def initialize(max_size: 100)
    @pool = []
    @max_size = max_size
  end
  
  def acquire
    @pool.pop || create_new_result_object
  end
  
  def release(object)
    return unless @pool.size < @max_size
    
    object.reset!  # Clear previous data
    @pool.push(object)
  end
  
  def with_result_object
    obj = acquire
    begin
      yield obj
    ensure
      release(obj)
    end
  end
  
  private
  
  def create_new_result_object
    QueryResultObject.new
  end
end

# Usage in database operations
RESULT_POOL = QueryResultPool.new

def process_complex_query(sql)
  RESULT_POOL.with_result_object do |result_obj|
    rows = ActiveRecord::Base.connection.execute(sql)
    result_obj.process_rows(rows)
    result_obj.aggregate_data
  end
end

Production Patterns

Production database optimization in Ruby applications requires comprehensive monitoring, automated scaling, and robust error recovery mechanisms. Production patterns focus on reliability, observability, and performance under real-world load conditions.

# Production-ready database configuration
class ProductionDatabaseConfig
  def self.configure
    ActiveRecord::Base.configurations['production'] = {
      'adapter' => 'postgresql',
      'database' => ENV['DATABASE_NAME'],
      'host' => ENV['DATABASE_HOST'],
      'username' => ENV['DATABASE_USER'],
      'password' => ENV['DATABASE_PASSWORD'],
      'pool' => ENV.fetch('DATABASE_POOL_SIZE', 25).to_i,
      'timeout' => 5000,
      'checkout_timeout' => 5,
      'reaping_frequency' => 10,
      'idle_timeout' => 300,
      'variables' => {
        'statement_timeout' => '30s',
        'lock_timeout' => '10s',
        'idle_in_transaction_session_timeout' => '60s'
      },
      'prepared_statements' => true,
      'advisory_locks' => true
    }
  end
  
  def self.configure_read_replica
    ActiveRecord::Base.configurations['production_replica'] = 
      ActiveRecord::Base.configurations['production'].merge(
        'host' => ENV['DATABASE_REPLICA_HOST'],
        'replica' => true
      )
  end
end

Health checking and monitoring systems track database performance metrics and alert on anomalies. Production applications implement comprehensive observability.

# Database health monitoring
class DatabaseHealthChecker
  HEALTH_METRICS = %w[
    connection_count
    active_queries
    long_running_queries
    deadlocks
    cache_hit_ratio
    index_usage
  ].freeze
  
  def self.check_health
    health_status = {}
    
    # Check connection pool status
    pool = ActiveRecord::Base.connection_pool
    health_status[:connection_pool] = {
      size: pool.size,
      checked_out: pool.checked_out.size,
      available: pool.available_connection_count,
      status: pool.available_connection_count > 2 ? 'healthy' : 'degraded'
    }
    
    # Check for long-running queries
    long_queries = get_long_running_queries
    health_status[:long_running_queries] = {
      count: long_queries.size,
      status: long_queries.size > 5 ? 'warning' : 'healthy',
      queries: long_queries.first(3)  # Sample of problematic queries
    }
    
    # Check database performance metrics
    health_status[:performance] = get_database_performance_metrics
    
    health_status
  end
  
  def self.get_long_running_queries
    ActiveRecord::Base.connection.execute(<<~SQL).to_a
      SELECT 
        query_start,
        state,
        query,
        EXTRACT(EPOCH FROM (now() - query_start)) as duration_seconds
      FROM pg_stat_activity 
      WHERE state = 'active' 
        AND query_start < now() - interval '30 seconds'
        AND query NOT LIKE '%pg_stat_activity%'
      ORDER BY query_start
    SQL
  end
  
  def self.get_database_performance_metrics
    cache_hit_ratio = ActiveRecord::Base.connection.execute(<<~SQL).first
      SELECT 
        ROUND(
          sum(blks_hit) * 100.0 / sum(blks_hit + blks_read), 2
        ) as cache_hit_ratio
      FROM pg_stat_database
    SQL
    
    {
      cache_hit_ratio: cache_hit_ratio['cache_hit_ratio'],
      status: cache_hit_ratio['cache_hit_ratio'].to_f > 95 ? 'healthy' : 'warning'
    }
  end
end

Automated failover and circuit breaker patterns provide resilience against database failures and performance degradation.

# Circuit breaker for database operations
class DatabaseCircuitBreaker
  def initialize(failure_threshold: 5, timeout: 30, monitor_window: 60)
    @failure_threshold = failure_threshold
    @timeout = timeout
    @monitor_window = monitor_window
    @failure_count = 0
    @last_failure_time = nil
    @state = :closed  # :closed, :open, :half_open
  end
  
  def call(&block)
    case @state
    when :open
      if Time.current - @last_failure_time > @timeout
        attempt_reset
      else
        raise CircuitBreakerOpenError, "Circuit breaker is open"
      end
    when :half_open
      attempt_call(&block)
    else  # :closed
      attempt_call(&block)
    end
  end
  
  private
  
  def attempt_call(&block)
    result = yield
    on_success
    result
  rescue => error
    on_failure(error)
    raise
  end
  
  def on_success
    @failure_count = 0
    @state = :closed
  end
  
  def on_failure(error)
    @failure_count += 1
    @last_failure_time = Time.current
    
    if @failure_count >= @failure_threshold
      @state = :open
      Rails.logger.error "Circuit breaker opened due to #{@failure_count} failures"
    end
  end
  
  def attempt_reset
    @state = :half_open
  end
end

# Usage in production database operations
DATABASE_CIRCUIT_BREAKER = DatabaseCircuitBreaker.new

def safe_database_operation
  DATABASE_CIRCUIT_BREAKER.call do
    # Potentially failing database operation
    yield
  end
rescue CircuitBreakerOpenError => e
  # Fallback behavior when database is unavailable
  Rails.logger.warn "Database unavailable, using fallback: #{e.message}"
  return_cached_result_or_default
end

Load balancing and read/write splitting distribute database load across multiple servers to improve performance and availability.

# Read/write splitting with automatic failover
module DatabaseLoadBalancer
  extend ActiveSupport::Concern
  
  included do
    connects_to database: { 
      writing: :primary, 
      reading: :replica 
    }
  end
  
  class_methods do
    def with_read_fallback(&block)
      connected_to(role: :reading) do
        yield
      end
    rescue ActiveRecord::ConnectionNotEstablished,
           ActiveRecord::StatementInvalid => e
      Rails.logger.warn "Read replica failed, falling back to primary: #{e.message}"
      
      connected_to(role: :writing) do
        yield
      end
    end
    
    def with_write_primary(&block)
      connected_to(role: :writing) do
        yield
      end
    end
    
    def balanced_read(preference: :replica)
      case preference
      when :replica
        with_read_fallback { yield }
      when :primary
        with_write_primary { yield }
      when :random
        [true, false].sample ? with_read_fallback { yield } : with_write_primary { yield }
      end
    end
  end
end

Reference

Core Classes and Modules

Class/Module Purpose Key Methods
ActiveRecord::Base Base class for models establish_connection, connection, transaction
ActiveRecord::ConnectionPool Manages database connections checkout, checkin, with_connection
ActiveRecord::QueryMethods Query building interface includes, joins, select, where, limit
ActiveRecord::Relation Represents database queries to_sql, explain, find_each, in_batches

Query Optimization Methods

Method Parameters Returns Description
includes(*associations) Association names ActiveRecord::Relation Eager loads associations to prevent N+1 queries
joins(*associations) Association names or SQL ActiveRecord::Relation Performs database joins
select(*fields) Column names ActiveRecord::Relation Specifies columns to retrieve
find_each(options = {}) batch_size, start, finish Enumerator Processes records in batches
in_batches(options = {}) batch_size, start, finish ActiveRecord::Relation Returns batched relations
pluck(*column_names) Column names Array Retrieves specific column values directly

Connection Pool Configuration

Option Type Default Description
pool Integer 5 Maximum number of connections in pool
timeout Integer 5000 Connection acquisition timeout (ms)
checkout_timeout Integer 5 Time to wait for connection (seconds)
reaping_frequency Integer 60 Frequency of connection reaping (seconds)
idle_timeout Integer 300 Time before idle connections are reaped
prepared_statements Boolean true Enable prepared statement caching

Performance Monitoring Methods

Method Returns Description
ActiveRecord::Base.connection_pool.stat Hash Connection pool statistics
Rails.cache.stats Hash Cache hit/miss statistics
ActiveRecord::QueryCache.cache Block result Enables query caching for block
ActiveRecord::Base.logger Logger Database query logger

Cache Configuration Options

Option Type Description
expires_in Duration Cache entry expiration time
race_condition_ttl Duration Time to serve stale cache during refresh
compress Boolean Enable cache value compression
namespace String Cache key namespace

Database-Specific Optimizations

# PostgreSQL-specific optimizations
class PostgreSQLOptimizer
  # EXPLAIN ANALYZE for query analysis
  def self.analyze_query(sql)
    ActiveRecord::Base.connection.execute("EXPLAIN ANALYZE #{sql}")
  end
  
  # Vacuum and analyze tables
  def self.maintenance(table_name)
    ActiveRecord::Base.connection.execute("VACUUM ANALYZE #{table_name}")
  end
  
  # Index usage statistics
  def self.index_usage_stats
    ActiveRecord::Base.connection.execute(<<~SQL)
      SELECT 
        schemaname, tablename, indexname,
        idx_tup_read, idx_tup_fetch,
        idx_tup_read::float / GREATEST(idx_tup_fetch, 1) as selectivity
      FROM pg_stat_user_indexes
      ORDER BY idx_tup_read DESC
    SQL
  end
end

Common Performance Patterns

# Counter cache maintenance
class CounterCacheManager
  def self.reset_all_counters
    User.reset_counters(User.pluck(:id), :posts)
    Post.reset_counters(Post.pluck(:id), :comments)
  end
end

# Bulk operations
class BulkOperations
  def self.bulk_insert(model_class, attributes_array)
    model_class.insert_all(attributes_array)
  end
  
  def self.bulk_update(model_class, updates_hash)
    model_class.upsert_all(updates_hash, unique_by: :id)
  end
end

Error Types and Handling

Exception Cause Typical Response
ActiveRecord::ConnectionTimeoutError Pool exhaustion Retry with backoff
ActiveRecord::Deadlocked Transaction deadlock Retry with random delay
ActiveRecord::StatementInvalid SQL syntax/constraint error Log and handle gracefully
ActiveRecord::RecordNotUnique Unique constraint violation Handle duplicate detection
ActiveRecord::ConnectionNotEstablished Database unavailable Failover to replica or cache