DEV Community

Deep Fix
Deep Fix

Posted on

Ultimate PostgreSQL Query Performance Optimization Guide for Developers

Introduction

PostgreSQL is a powerhouse for relational data, but poorly written queries can turn that power into a bottleneck. In this guide we dive into practical techniques to boost query speed, backed by real‑world examples and step‑by‑step troubleshooting.


1. Read the Execution Plan First

The fastest way to understand why a query is slow is to look at its execution plan.

EXPLAIN (ANALYZE, BUFFERS) 
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > CURRENT_DATE - INTERVAL '30 days';
Enter fullscreen mode Exit fullscreen mode

Key columns in the output:

  • Seq Scan vs Index Scan – indicates whether an index is being used.
  • Rows Removed by Filter – shows excessive filtering after a scan.
  • Total Runtime – the actual time spent.

If you see a Seq Scan on a large table, consider adding an index (see section 2).


2. Indexing Strategies that Actually Help

2.1 Simple B‑Tree Index

CREATE INDEX idx_orders_created_at ON orders (created_at);
Enter fullscreen mode Exit fullscreen mode

2.2 Composite Index for Join + Filter

When a query filters on created_at and joins on customer_id, a composite index can cover both operations:

CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at);
Enter fullscreen mode Exit fullscreen mode

2.3 Partial Indexes for Hot Subsets

If you only ever query recent orders, a partial index keeps the index small and fast:

CREATE INDEX idx_recent_orders ON orders (customer_id)
WHERE created_at > CURRENT_DATE - INTERVAL '90 days';
Enter fullscreen mode Exit fullscreen mode

3. Query Rewriting Tips

  1. Avoid SELECT * – fetch only needed columns.
  2. Use EXISTS instead of IN when the sub‑query returns many rows.
  3. Leverage CTEs wisely – materialize only when necessary (WITH ... MATERIALIZED).
  4. Apply LIMIT early if you only need a subset.
-- Bad: SELECT * with IN
SELECT * FROM products p
WHERE p.id IN (SELECT product_id FROM sales WHERE amount > 1000);

-- Good: EXISTS with column list
SELECT p.id, p.name, p.price
FROM products p
WHERE EXISTS (
    SELECT 1 FROM sales s WHERE s.product_id = p.id AND s.amount > 1000
);
Enter fullscreen mode Exit fullscreen mode

4. Configuration Tuning (PostgreSQL Settings)

Parameter Typical Adjustment Effect
work_mem SET work_mem = '64MB'; (per session) Allows larger hash tables, faster sorts.
shared_buffers 25‑30% of RAM Improves cache hit ratio.
effective_cache_size Approx. 50‑75% of RAM Helps planner estimate available cache.
max_parallel_workers_per_gather 2 or 4 Enables parallel query execution.
-- Example: increase work_mem for a heavy report
SET work_mem = '128MB';
SELECT * FROM big_report();
Enter fullscreen mode Exit fullscreen mode

5. Monitoring & Troubleshooting Workflow

  1. Identify the slow query – use pg_stat_statements or APM tools.
  2. Run EXPLAIN ANALYZE – capture plan and runtime.
  3. Check index usage – look for Seq Scan.
  4. Adjust indexes or rewrite – apply changes from sections 2‑3.
  5. Retest – repeat step 2 until runtime meets SLA.
  6. Persist configuration – add successful SET commands to postgresql.conf or connection pool settings.

Quick Checklist (copy‑paste into your incident report)

- Query text:
- EXPLAIN ANALYZE output (trimmed):
- Indexes present:
- work_mem / other GUC values:
- Observed rows vs estimated rows:
- Action taken:
Enter fullscreen mode Exit fullscreen mode

6. Real‑World Example: Fixing a 30‑second Report

The following repository contains a pre‑configured script that applies the exact fixes described above:

Running the script creates the recommended indexes and adjusts work_mem for the session, slashing the report time from 30 s to ≈2 s on a 200 M‑row table.


Conclusion

Optimizing PostgreSQL query performance is a blend of observability, smart indexing, thoughtful query design, and tuned configuration. By following the systematic approach above, developers, engineers, and DevOps teams can consistently shave seconds—or even minutes—off critical workloads.

Happy querying!

Top comments (0)