Engineering Series

This post is part of the Distributed Database Systems: A Senior Engineer’s Guide series.

Debugging Slow Database Queries: A Systematic Approach

Learn how to identify and fix slow database queries using query plans, indexes, and optimization techniques from real production scenarios.

January 9, 20264 min read

Part of the Pillar: Distributed Systems Failure Modes. This post covers systematic query optimization techniques. For architectural foundations, start with Why HTAP Systems Fail Quietly.

Slow queries are inevitable at scale. Here's my framework for debugging them efficiently, based on years of supporting production databases.

The 4-Step Framework

1. Identify the Bottleneck

Don't assume it's the database! Check:

  • Application code: Is the query being called in a loop?
  • Network: High latency between app and database?
  • Database: Is the query itself slow?
bash
# Quick network latency check
ping your-database-host.com

# Expected: < 5ms for same region
# Warning: > 50ms suggests network issues

2. Capture the Query Plan

The query plan shows how the database executes your query.

sql
-- PostgreSQL / AWS RDS
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.order_id) as order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.created_at > '2025-01-01'
GROUP BY u.user_id;

Key indicators:

  • Seq Scan on large tables → Missing index
  • Nested Loop with high row counts → Join inefficiency
  • Sort operation → Consider covering indexes

3. Fix Access Patterns

Common fixes:

Add Missing Indexes

sql
-- Before: Sequential scan on 10M rows
-- After: Index scan on 1K rows
CREATE INDEX idx_users_created_at ON users(created_at);

Optimize Joins

sql
-- Bad: Large intermediate result set
SELECT *
FROM orders o
JOIN products p ON o.product_id = p.product_id
WHERE o.created_at > '2025-01-01';

-- Good: Filter first, then join
SELECT *
FROM (
  SELECT * FROM orders WHERE created_at > '2025-01-01'
) o
JOIN products p ON o.product_id = p.product_id;

Use Covering Indexes

sql
-- Query needs: user_id, email, created_at
CREATE INDEX idx_users_covering ON users(user_id, email, created_at);

-- Database can satisfy query entirely from index (no table lookup)

4. Validate Under Load

Test the fix in a staging environment with realistic data volume:

bash
# Before optimization
ab -n 1000 -c 10 http://api.example.com/users

# After optimization
ab -n 1000 -c 10 http://api.example.com/users

Real-World Case Study

Problem

Dashboard query taking 45 seconds during peak hours:

sql
SELECT DATE(created_at) as day, COUNT(*) as orders
FROM orders
WHERE status = 'completed'
GROUP BY DATE(created_at)
ORDER BY day DESC
LIMIT 30;

Root Cause

  1. No index on status
  2. DATE() function prevents index usage
  3. Full table scan on 100M+ rows

Solution

sql
-- Add functional index
CREATE INDEX idx_orders_completed_date 
ON orders(DATE(created_at)) 
WHERE status = 'completed';

-- Or better: Store computed column
ALTER TABLE orders ADD COLUMN created_date DATE 
  GENERATED ALWAYS AS (DATE(created_at)) STORED;

CREATE INDEX idx_orders_status_date ON orders(status, created_date);

Result: Query time reduced from 45s → 120ms

Advanced Techniques

Partition Large Tables

sql
-- Partition by month for time-series data
CREATE TABLE orders (
  order_id BIGINT,
  created_at TIMESTAMP,
  ...
) PARTITION BY RANGE (created_at);

-- Create partitions
CREATE TABLE orders_2025_01 PARTITION OF orders
  FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

Use Materialized Views

sql
-- Pre-compute expensive aggregations
CREATE MATERIALIZED VIEW daily_sales AS
SELECT DATE(created_at) as day, SUM(total) as revenue
FROM orders
GROUP BY DATE(created_at);

-- Refresh hourly
REFRESH MATERIALIZED VIEW daily_sales;

Query Rewriting

sql
-- Slow: DISTINCT on large table
SELECT DISTINCT user_id FROM orders;

-- Fast: Use EXISTS instead
SELECT user_id FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);

Monitoring Checklist

Set up alerts for:

  • Queries taking > 1 second
  • High CPU usage (> 80%)
  • Lock waits
  • Index bloat
  • Replication lag
python
# Example monitoring query
def identify_slow_queries():
    return db.execute("""
        SELECT query, mean_exec_time, calls
        FROM pg_stat_statements
        WHERE mean_exec_time > 1000
        ORDER BY mean_exec_time DESC
        LIMIT 10
    """)

Takeaways

  1. Always check the query plan before optimizing
  2. Index strategically, not excessively
  3. Test with production-like data volume
  4. Monitor in production to catch regressions

Want to dive deeper? Check out my post on Why HTAP Systems Fail Quietly.

Shahid Moosa

About the Author

Shahid Moosa is a Cloud Database Support Engineer specializing in distributed systems, AWS, and SingleStore. He helps teams build scalable, reliable data infrastructure.

Get in touch →

Share this briefing

© 2026 Shahid Moosa. Built with React & Tailwind CSS.