StormaticsStormatics

Fixing ORM Slowness by 80% with Strategic PostgreSQL Indexing

Modern applications heavily rely on ORMs (Object-Relational Mappers) for rapid development. While ORMs accelerate development, they often generate queries that are not fully optimized for database performance. In such environments, database engineers have limited control over query structure, leaving indexing and database tuning as the primary performance optimization tools.

In this article, I’ll share how we improved PostgreSQL performance dramatically for one of our enterprise customers by applying strategic indexing techniques,without modifying application queries.

The Challenge: High Read IOPS and Slow Query Performance

One of our customers experienced severe performance degradation, including:

  • High Read IOPS on the database
  • Slow page loads and delayed reports
  • Increasing database load during peak hours

After analyzing PostgreSQL configuration parameters, we confirmed that:

  • Memory parameters were properly tuned
  • Autovacuum was functioning correctly
  • Hardware resources were sufficient

However, performance issues persisted.

Since the application relied entirely on ORM-generated queries, rewriting queries was not an option. We needed a solution at the database level.

Root Cause Analysis: Excessive Sequential Scans

We analyzed PostgreSQL statistics using:

  • pg_stat_user_tables
  • pg_stat_user_indexes
  • pg_constraint
  • pg_index

We discovered extremely high sequential scans on large tables—some exceeding 41 million scans.

Sequential scans on large tables significantly increase disk I/O and slow query execution.

The primary reason: Missing indexes on foreign key columns and frequently filtered columns.

Strategy #1: Foreign Key Index Optimization

Why this matters

PostgreSQL does NOT automatically create indexes on foreign key columns.

Without these indexes, PostgreSQL must perform sequential scans when:

  • Joining tables
  • Filtering by foreign keys
  • Enforcing referential integrity

This is especially critical in ORM-based systems, where joins on foreign keys are extremely common.

How we identified missing FK indexes

We ran the following query to detect missing indexes on foreign key columns in large, frequently scanned tables:

WITH high_seq_tables AS (
SELECT
     st.relid,
     st.schemaname,
     st.relname AS table_name,
     st.seq_scan,
       pg_total_relation_size(st.relid) AS table_size_bytes
FROM pg_stat_user_tables st
WHERE st.seq_scan > 10000
   AND pg_total_relation_size(st.relid) >= 524288000
),
fk_columns AS (
SELECT
     con.conrelid,
     att.attname AS fk_column
FROM pg_constraint con
JOIN unnest(con.conkey) AS colnum(colnum) ON true
JOIN pg_attribute att
     ON att.attrelid = con.conrelid
    AND att.attnum = colnum.colnum
WHERE con.contype = 'f'
),
fk_index_check AS (
SELECT
     fk.conrelid,
     fk.fk_column,
     NOT EXISTS (
         SELECT 1
         FROM pg_index idx
         JOIN pg_attribute ia
             ON ia.attrelid = idx.indrelid
            AND ia.attnum = ANY(idx.indkey)
         WHERE idx.indrelid = fk.conrelid
           AND idx.indisvalid
           AND ia.attname = fk.fk_column
     ) AS is_missing_fk_index
FROM fk_columns fk
)
SELECT *
FROM fk_index_check
WHERE is_missing_fk_index = true;

Results after implementing FK indexes

After creating indexes on critical foreign key columns:

  • Sequential scans reduced by over 87%
  • Query response time improved by 60-80%
  • Disk read IOPS dropped significantly
  • Overall system responsiveness improved dramatically

This was the single most impactful optimization.

Strategy #2: Slow Query Driven Index Optimization

Instead of blindly indexing everything, we followed a targeted approach using slow query analysis.

Step 1: Enable Slow Query Logging

SET log_min_duration_statement = 420000;

This logs queries taking longer than 7 minutes. You can adjust this threshold based on your workload.

Step 2: Identify Query Patterns

From slow query logs, we identified common patterns:

Columns frequently used in:

  • WHERE clauses
  • JOIN conditions
  • GROUP BY operations
  • Aggregations (AVG, COUNT, SUM)

Step 3: Create Targeted Indexes

We created indexes on:

  • Foreign key columns
  • Join columns
  • Filter columns
  • Frequently aggregated columns

Example:

CREATE INDEX CONCURRENTLY idx_orders_customer_id ON orders_main(customer_id);

Index Usage Validation

After creating indexes, we verified usage via:

SELECT *
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;

This helped ensure indexes were actually used and provided performance benefits.

Overall Performance Improvements

Metric

Improvement

Sequential Scans

↓ 87%

Query Execution Time

↓ 60–80%

Disk Read IOPS

↓ 70%

Application Response Time

Dramatically Improved

Key Lessons Learned

  • Always index foreign key columns in ORM-driven applications
  • Use PostgreSQL statistics views to identify missing indexes
  • Enable slow query logging to identify optimization opportunities
  • Create targeted indexes, not excessive indexes
  • Continuously monitor index usage and performance

Final Thoughts

When application-level optimization is limited due to ORM constraints, database-level indexing becomes the most powerful performance optimization tool.

Strategic indexing can dramatically improve PostgreSQL performance without modifying application code or upgrading hardware.

Leave A Comment