DEV Community

Cover image for Database optimization best practices
Divyansh Gupta
Divyansh Gupta

Posted on

Database optimization best practices

Imagine your database as a wild animal sanctuary: some queries lumber like tortoises, while others sprint like cheetahs. Your job as a DBA is to coax every query into channeling its inner cheetah—fast, efficient, and resource-savvy. In this KB article, you’ll discover practical techniques, vibrant code examples, ASCII-art execution plans, and Mermaid flowcharts that transform sluggish SQL into scalpels of performance. fileciteturn0file0


1. Measure Twice, Cut Once: EXPLAIN & ANALYZE

Before refactoring, know your enemy. Use:

EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;
Enter fullscreen mode Exit fullscreen mode

to expose hidden bottlenecks: row estimates, buffer hits vs. reads, and CPU vs. I/O costs.

                                     QUERY PLAN
-----------------------------------------------------------------------------------
 Hash Join  (cost=150.00..500.00 rows=1000 width=64) (actual time=12.345..45.678 rows=950 loops=1)
   Hash Cond: (t1.id = t2.foreign_id)
   Buffers: shared hit=2000 read=1500
Enter fullscreen mode Exit fullscreen mode

This tells you whether your query is CPU-bound, I/O-bound, or suffering from bad cardinality estimates.

Image description


2. Indexing Mastery: More Than Just B‑Trees

2.1 Partial & Expression Indexes

Target hot filter patterns without bloating:

CREATE INDEX idx_active_users ON users((lower(email)))
 WHERE status = 'active';
Enter fullscreen mode Exit fullscreen mode

2.2 BRIN for Time-Series

Massive append-only tables? Try BRIN:

CREATE TABLE logs (
  ts TIMESTAMPTZ,
  event JSONB
) PARTITION BY RANGE (ts);
CREATE INDEX ON logs USING BRIN (ts);
Enter fullscreen mode Exit fullscreen mode

This lightweight index slashes size at scale.


3. Encapsulate Complexity: Stored Functions & Views

Rather than embedding 10 JOINs in every API call, wrap logic in a function or view:

CREATE OR REPLACE FUNCTION daily_sales_summary(day DATE)
RETURNS TABLE(user_id UUID, total DECIMAL) AS $$
BEGIN
  RETURN QUERY
  SELECT s.user_id, SUM(amount)
  FROM sales s
  WHERE date_trunc('day', s.ts) = day
  GROUP BY s.user_id;
END;
$$ LANGUAGE plpgsql;
Enter fullscreen mode Exit fullscreen mode

The planner can optimize a stable function more aggressively than ad-hoc SQL.


4. Aggregations & Windows: Tricks of the Trade

4.1 Materialized Aggregates

For metrics dashboards, precompute:

CREATE MATERIALIZED VIEW mv_user_errors AS
SELECT user_id, COUNT(*) AS error_count
FROM events
WHERE error_flag
GROUP BY user_id;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_user_errors;
Enter fullscreen mode Exit fullscreen mode

4.2 Window Functions vs. GROUP BY

When you need both raw rows and aggregates:

SELECT
  order_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS total_per_customer
FROM orders;
Enter fullscreen mode Exit fullscreen mode

Use an index on (customer_id, amount) to speed windows.


5. Partitioning & Parallelism: Scale Out Safely

5.1 Declarative Partitioning

Split by time or key:

CREATE TABLE metrics (
  ts DATE,
  value DOUBLE PRECISION
) PARTITION BY RANGE (ts);
CREATE TABLE metrics_2025_q1 PARTITION OF metrics
 FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
Enter fullscreen mode Exit fullscreen mode

5.2 Harness Parallel Queries

Enable in postgresql.conf:

max_parallel_workers_per_gather = 4
Enter fullscreen mode Exit fullscreen mode

Then large scans auto-split across CPUs.


6. Housekeeping: VACUUM, ANALYZE & Maintenance

6.1 Autovacuum Tuning

Ensure autovacuum thresholds fit your workload. For high-churn tables:

ALTER TABLE big_table
SET ( autovacuum_vacuum_scale_factor = 0.05,
      autovacuum_analyze_scale_factor = 0.02 );
Enter fullscreen mode Exit fullscreen mode

6.2 Fillfactor for Write-Heavy Tables

Reserve free space to reduce page splits:

ALTER TABLE logs SET (fillfactor = 70);
Enter fullscreen mode Exit fullscreen mode

7. Real‑World Case Study: 80% Speedup

Scenario: A nightly report took 10 minutes. By applying:

  1. Partial index on status

  2. Function-based view

  3. Partition pruning on date

  4. Autovacuum tuning

we tracked its execution plan changes:

-- Before:
Seq Scan on orders  (time: 600s)
-- After:
Index Only Scan using idx_status_date  (time: 120s)
Enter fullscreen mode Exit fullscreen mode

From 10 min → 2 min: a success story to inspire your own triumphs.


Key Takeaways

  • Measure first with EXPLAIN ANALYZE (BUFFERS).

  • Index smartly: partial, expression, BRIN.

  • Encapsulate complex logic in functions/views.

  • Precompute heavy aggregates with materialized views.

  • Partition & parallelize for scale.

  • Maintain: VACUUM, ANALYZE, and fillfactor.

8. Beyond the Basics: Advanced Techniques

8.1 Adaptive Query Plans with pg_stat_statements

Track your most expensive statements:

CREATE EXTENSION pg_stat_statements;
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

Use this insight to prioritize optimizations.

8.2 Plan Stability with Prepared Statements

For queries with variable patterns, prepared statements lock in good plans:

PREPARE fast_search(text) AS
SELECT * FROM products WHERE description ILIKE $1;
EXECUTE fast_search('%widget%');
Enter fullscreen mode Exit fullscreen mode

8.3 In-Memory Speed with UNLOGGED Tables

Temp-heavy data can live in RAM:

CREATE UNLOGGED TABLE temp_hits AS
SELECT ...;
Enter fullscreen mode Exit fullscreen mode

8.4 Smart Caching Layers

Combine Redis or PGSQL's native caching:

DO $$
BEGIN
  PERFORM pg_prewarm('hot_table');
END;
$$;
Enter fullscreen mode Exit fullscreen mode

Creative Corner: Visualizing Data Flow

Image description

Bring your diagrams to life—they guide both your brain and your team.


Final Thoughts: The Art of Performance

Optimizing SQL is equal parts science and art. It’s a continuous journey: measure, tweak, observe, and repeat. With these techniques—from core index strategies to creative caching and plan management—you’re equipped to turn any tortoise into a cheetah.
Remember: the fastest query is the one you never run. Cache wisely, precompute where it counts, and let your database shine.

Top comments (0)