Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

How-tos

Your Database Is Screaming — Here’s How to Finally Make It Purr

A practical guide to identifying and fixing slow database queries, covering logging, indexing, pagination, N+1 problems, connection pooling, read replicas, and caching for better performance.

June 2026 · 8 min read · 1 views · 0 hearts

Your Database Is Screaming — Here’s How to Finally Make It Purr

There’s nothing quite like watching your web app grind to a halt because one query takes twelve seconds. The coffee gets cold. Users abandon your site. You start questioning your life choices. But here’s the truth: slow queries aren’t a mystery — they’re a solvable engineering problem. Let’s cut through the noise and fix them.

Step 1: Find the Culprit Before You Fix It

You can’t optimize what you can’t see. Your first move? Enable slow query logging.

Most databases have it built-in. For PostgreSQL, set log_min_duration_statement = 200 in your config (that’s 200ms). MySQL has slow_query_log = 1. Let it run for a day, then grep through the logs. You’ll often find the same query — or same table — choking your app.

Once you spot the offender, use EXPLAIN ANALYZE (PostgreSQL, MySQL, SQLite all support it). This shows you exactly how the database executes your query, line by line. Look for: - Seq Scan — reading every row in the table (bad for large tables) - Nested Loop — joining tables one row at a time (can be fine, but watch for millions of loops) - Sort — sorting in memory or on disk (often points to missing indexes)

Step 2: Indexes Are Your Best Friend (and Your Worst Enemy)

Everyone knows indexes speed up reads. But the trick is getting the right ones.

Bad index:

CREATE INDEX idx_users_name ON users(name);

This helps if you filter by name exactly. But what about WHERE name LIKE 'A%'? That's fine. What about WHERE email = 'foo@bar.com'? That index is useless.

Better approach: Index the columns you actually filter on. If you constantly query:

SELECT * FROM orders WHERE user_id = 42 AND created_at > '2024-01-01';

Create a composite index:

CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);

The order matters: put the equality column first (user_id), then the range column (created_at). PostgreSQL and MySQL will use the index much more efficiently.

📌 Pro tip: Don’t over-index. Every index slows down writes (INSERTs and UPDATEs). On write-heavy tables, 2–3 indexes max. On read-heavy tables, 5–6 can be fine.

Step 3: Stop Fetching Everything — Select Only What You Need

This one’s embarrassingly common:

SELECT * FROM articles WHERE id = 137;

You just shipped the entire row across the wire — all 20 columns — when your frontend only needs title and updated_at. That wastes bandwidth and memory.

Fix it:

SELECT title, updated_at FROM articles WHERE id = 137;

On a small app, this is trivial. On a table with 10 million rows and heavy traffic, it can halve your query time.

Step 4: Pagination Done Right — Stop Using OFFSET

If you’re using LIMIT 10 OFFSET 10000, you’re forcing the database to scan and discard 10,000 rows before returning data. It gets worse as you page deeper.

Better approach: Use keyset pagination (also called “cursor-based pagination”):

SELECT * FROM articles WHERE id > 9990 ORDER BY id LIMIT 10;

This uses the index on id directly — no scanning. Works for timestamps too, as long as you have an index.

Step 5: The N+1 Query Problem — Slay This Beast

You see it often in ORMs like Django or ActiveRecord:

articles = Article.objects.all()   # 1 query
for article in articles:
    print(article.author.name)     # N queries (one per article)

This gives you 1 + N database round-trips. For 100 articles, that's 101 queries.

Fix it: Use select_related() (for foreign keys) or prefetch_related() (for many-to-many) to fetch related data in one query:

articles = Article.objects.select_related('author').all()

Now it's one query with a JOIN. Or two, if you use prefetch. Either way, far faster.

Step 6: Connection Pooling — Stop Starting Fresh Every Time

Every query involves opening a database connection, which takes time — 5–30ms depending on network. If your app opens and closes connections for every request, you’re wasting precious milliseconds.

Use a connection pooler: - PostgreSQL: PgBouncer or built-in psycopg2 pool - MySQL: MySQLdb pool or external proxy - Python ORMs: Django and SQLAlchemy both support pool sizes like 15–30

Set a reasonable pool size (e.g., max_connections=20) and reuse connections. You’ll shave off 20–50ms per request instantly.

Step 7: Read Replicas — When One Database Isn’t Enough

If your database is hammered by both writes and reads, split the load: - Master database handles INSERTs, UPDATEs, DELETEs - Read replicas handle SELECTs

In Django, you can route queries:

class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        return 'replica'
    def db_for_write(self, model, **hints):
        return 'default'

Most web frameworks have similar patterns. This scales horizontally without touching your code logic.

Step 8: The Humble Query Cache (Use With Caution)

Some databases (MySQL with query cache enabled) or Redis can cache query results. If you have a query that runs once per second and the data changes only once a day, cache the result:

import redis
r = redis.Redis()
cached = r.get('dashboard_stats')
if cached:
    return cached
# else run expensive query
result = db.query("SELECT ...")
r.setex('dashboard_stats', 3600, result)

Watch out: Stale data can bite you. Use short TTLs (1–60 seconds) for dynamic data, and long TTLs for dashboard or static reports.

Final Takeaway

You don’t need to be a DBA to fix slow queries. Start with logging. Add the right indexes. Cut down on data fetched. Paginate with cursors. Use connection pools. And when all else fails, add a read replica.

One or two of these changes can turn a 12-second query into 30ms. Your users will notice — and so will your blood pressure.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.