Database Performance

Database Index Design Patterns: Composite, Covering, and Partial Indexes

Master database index design: composite index column order, covering indexes with INCLUDE, partial indexes for filtered queries, functional indexes, and unused index auditing.

JusDB Team
May 14, 2025
5 min read
155 views

The right indexes make queries orders of magnitude faster; the wrong ones waste disk space and slow down writes. Here are the most important index design patterns for MySQL and PostgreSQL.

Composite Index Column Order

Columns in a composite index should be ordered: equality first, then range, then sort.

sql
-- Query: WHERE status = 'active' AND created_at > '2025-01-01' ORDER BY created_at DESC

-- Good: equality (status) first, then range/sort (created_at)
CREATE INDEX idx_orders_status_created ON orders (status, created_at DESC);

-- Bad: range column first — wastes the index for the equality filter
CREATE INDEX idx_orders_bad ON orders (created_at, status);

Covering Indexes (Index-Only Scans)

sql
-- Include all columns needed by the query to avoid heap lookups
-- Query: SELECT user_id, total FROM orders WHERE status = 'pending'

-- PostgreSQL: use INCLUDE for non-searchable columns
CREATE INDEX idx_orders_status_cover ON orders (status) INCLUDE (user_id, total);

-- MySQL: add to the index key itself
CREATE INDEX idx_orders_status_cover ON orders (status, user_id, total);

Partial Indexes

sql
-- PostgreSQL: index only active orders (much smaller, faster)
CREATE INDEX idx_orders_active ON orders (created_at)
  WHERE status = 'active';

-- Only 2% of orders may be active — index is 50x smaller than full index

Functional Indexes

sql
-- PostgreSQL: case-insensitive email lookups
CREATE INDEX idx_users_email_lower ON users (lower(email));
SELECT * FROM users WHERE lower(email) = lower('User@Example.com');

-- MySQL: expression index (MySQL 8.0+)
CREATE INDEX idx_users_email_lower ON users ((lower(email)));

Find Missing Indexes

sql
-- PostgreSQL: tables with sequential scans
SELECT schemaname, relname, seq_scan, idx_scan,
       seq_scan - idx_scan AS missing_idx_approx
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan AND n_live_tup > 10000
ORDER BY seq_scan DESC
LIMIT 10;

-- MySQL: check slow query log for full table scans
-- Or use pt-query-digest to find top offenders

Find Unused Indexes

sql
-- PostgreSQL: indexes with zero scans since last stats reset
SELECT schemaname, relname, indexrelname,
       idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
Tip: Reset index statistics after major schema changes: SELECT pg_stat_reset(); Then wait 2 weeks before dropping unused indexes — low-frequency queries may not have run yet.

Key Takeaways

  • Composite indexes: equality columns first, then range, then sort columns
  • Covering indexes eliminate heap lookups — use INCLUDE in PostgreSQL
  • Partial indexes are dramatically smaller for filtered queries (e.g., active records only)
  • Audit unused indexes quarterly — each unused index costs write overhead and disk space

JusDB Can Help

Index design is one of the highest-impact performance optimizations. JusDB can audit your indexes, identify missing ones, and remove costly unused indexes.

Share this article

JusDB Team

Official JusDB content team