Database & Cloud

Eliminating Database Bottlenecks: Indexing Strategies, Execution Plans (EXPLAIN ANALYZE), and Query Optimization in PostgreSQL & MySQL

Laurince Quijano
Laurince Quijano
July 27, 20267 min read
Eliminating Database Bottlenecks: Indexing Strategies, Execution Plans (EXPLAIN ANALYZE), and Query Optimization in PostgreSQL & MySQL

As web applications scale to millions of records, database performance becomes the single most critical factor determining application latency. A single unindexed database query can turn a 50ms API endpoint into a 5-second server timeout, consuming server CPU and driving up cloud infrastructure costs.

While ORMs like Prisma, TypeORM, and Eloquent make querying databases convenient, they can obscure inefficient SQL execution strategies under the hood—such as N+1 query loops, missing index scans, and expensive full-table scans.

To optimize database workloads, developers must master reading Query Execution Plans (`EXPLAIN ANALYZE`) and applying Strategic Database Indexing.

Here is an architectural guide to diagnosing and eliminating database bottlenecks in PostgreSQL and MySQL.


Step 1: Identifying Slow Queries with EXPLAIN ANALYZE

Before adding database indexes indiscriminately, you must inspect how the database engine plans and executes your SQL queries. Prefixing any SQL statement with EXPLAIN ANALYZE reveals the actual execution tree, node costs, memory usage, and execution time:

sql
EXPLAIN ANALYZE
SELECT o.id, o.total_amount, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'completed'
  AND o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 50;

Key Execution Plan Metrics to Inspect:

Execution NodeWhat it MeansAction Required
Seq Scan (Sequential Scan)The database scanned every single row in the table sequentially.High Risk: Missing index on filtered columns (WHERE).
Index ScanThe engine traversed a B-Tree index to locate target row pointers.Optimal: Index is being utilized efficiently.
Index Only ScanThe query returned data directly from the index without reading table pages.Best Performance: Index contains all requested columns.
Nested Loop / Hash JoinExecution strategy used to combine rows from multiple tables.Check join key indexes if memory usage spikes.

Step 2: Strategic Indexing Patterns

Indexes use B-Tree (Balanced Tree) data structures to organize column values in sorted order, reducing search complexity from $O(N)$ full-table scans to $O(log N)$ logarithmic lookups.

1. Single-Column B-Tree Indexes Ideal for columns frequently queried in WHERE clauses, foreign keys, or unique identifiers:

sql
-- Index foreign key column to accelerate JOIN operations
CREATE INDEX idx_orders_user_id ON orders(user_id);

2. Composite (Multi-Column) Indexes & Left-Prefix Rule When queries filter across multiple columns (e.g. status AND created_at), a single multi-column composite index is significantly faster than two separate single-column indexes.

Critical Rule (Left-Most Prefix): Column order in a composite index matters. Place high-cardinality or equality-filtered columns first, followed by range filters or sorting columns:

sql
-- Optimal composite index for filtering status + sorting created_at
CREATE INDEX idx_orders_status_created_at ON orders(status, created_at DESC);

3. Covering Indexes (INCLUDE Clause in PostgreSQL) A covering index satisfies the entire query from the index alone, bypassing table heap lookups entirely:

sql
-- Include total_amount in index payload to avoid reading table pages
CREATE INDEX idx_orders_covering ON orders(status, created_at DESC) INCLUDE (total_amount);

Step 3: Common Query Anti-Patterns to Avoid

1. Applying Functions to Indexed Columns Wrapping an indexed column inside a function prevents the database engine from using the index:

sql
-- BAD (Bypasses Index): Function applied to column
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';

-- GOOD (Uses Index): Compare directly or use functional index
SELECT * FROM users WHERE email = 'user@example.com';

-- ALTERNATIVE: Create a Functional Index in Postgres
CREATE INDEX idx_users_lower_email ON users(LOWER(email));

2. Wildcard Leading Searches (LIKE '%term') Leading wildcards prevent B-Tree index traversal because the starting character is unknown:

sql
-- BAD (Triggers Full-Table Scan): Leading wildcard
SELECT * FROM products WHERE name LIKE '%phone';

-- GOOD (Uses Trigram / GIN Index in PostgreSQL for text search):
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_products_name_trgm ON products USING gin (name gin_trgm_ops);

Summary Checklist for Database Tuning

  1. Log Slow Queries: Enable pg_stat_statements in PostgreSQL or slow_query_log in MySQL to surface queries exceeding 100ms thresholds.
  2. Analyze Execution Trees: Run EXPLAIN ANALYZE on slow queries to identify Sequential Scans.
  3. Build Target Composite Indexes: Order composite index fields following the Left-Most Prefix rule (Equality $ ightarrow$ Range/Sort).
  4. Monitor Index Usage & Over-Indexing: Unused indexes degrade INSERT, UPDATE, and DELETE throughput. Regularly prune unused indexes using pg_stat_user_indexes.

By profiling execution trees and applying targeted composite indexing, database response times drop from seconds to milliseconds, ensuring your web applications handle massive traffic gracefully!

Laurince Quijano
Written By

Laurince Quijano

Full-Stack Architect

Award-winning Web Developer with 10+ years of experience. Specializing in enterprise Next.js performance, custom Magento plugins, API integrations, and Technical SEO infrastructure.

Need premium developer consulting?

Let's discuss how we can build API split checkouts, dynamic Next.js interfaces, or speed audits for your business.

Get in Touch