Cutting Index Size and Boosting Speed with PostgreSQL Partial Indexes
Partial indexes let you index only the rows you query most, cutting disk usage and speeding up reads. Learn when and how to create them, verify planner usage, and watch for maintenance pitfalls.
31 Aug 2025, 13:33 UTC

Why Partial Indexes Matter
When a table contains millions of rows but only a small, frequently queried subset is of interest, a full index can become a waste of disk space and maintenance time. Partial indexes let you index just that subset, shrinking the index footprint and speeding up both reads and writes for the targeted data.
How Partial Indexes Work
A partial index is created with a WHERE clause that limits the rows stored in the index. PostgreSQL will only include rows that satisfy the predicate. The planner will automatically use the partial index when a query’s WHERE clause is a superset of the index predicate. For example:
CREATE INDEX idx_pending_orders ON orders (created_at)
WHERE status = 'pending';
Here, only rows where status equals pending are indexed. A query such as SELECT * FROM orders WHERE status = 'pending' AND created_at > now() - interval '1 day'; will use this index.
When to Use a Partial Index
- Skewed Boolean or Enum Columns: If only one flag value is queried most often (e.g.,
active = true), index just that value. - NULL‑Heavy Columns: Index non‑NULL values to avoid storing many NULL entries.
- High‑Cardinality Filters: When a WHERE clause filters on a column with a small set of frequent values.
- Maintenance Overhead: Fewer rows in the index mean less work when rows are inserted, updated, or deleted.
Concrete Example: Optimizing Order Retrieval
Suppose the orders table has 10 M rows, 90 % of which are completed. The application often pulls recent pending orders for processing. A full index on status, created_at would be large and updated for every order change. A partial index solves this.
- Create the partial index (run as a superuser or a role with
CREATE INDEXprivilege):
CREATE INDEX idx_pending_created_at ON orders (created_at)
WHERE status = 'pending';
- :
SELECT pg_size_pretty(pg_relation_size('idx_pending_created_at')) AS partial_index_size;
SELECT pg_size_pretty(pg_relation_size('orders')) AS table_size;
Expect the partial index to be a fraction of a full index that would cover all statuses.
- :
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE status = 'pending' AND created_at > now() - interval '1 day';
The planner should show an Index Scan using idx_pending_created_at with a low Rows Removed by Filter count.
Trade‑Offs and Limitations
- Predicate Coverage: If a query does not include the index predicate (e.g.,
WHERE status = 'pending'only), PostgreSQL will still use the partial index. But if the predicate is absent or different, it falls back to a sequential scan. - Maintenance Complexity: Multiple partial indexes can fragment the indexing strategy and make troubleshooting harder.
- Updates Crossing the Boundary: Changing a row’s
statusfrompendingtocompletedor vice versa causes an index update. Frequent status flips can reduce the benefit. - No Index for Other Values: Queries that need all statuses cannot use the partial index, so a complementary full or multi‑column index may still be necessary.
Practical Checklist Before Deployment
- Run
ANALYZEon the table to update statistics. - Use
EXPLAIN (ANALYZE, BUFFERS)to confirm the planner picks the partial index. - Monitor
pg_stat_user_indexesforidx_scanandidx_tup_readto gauge usage. - Periodically compare
pg_relation_sizeof the partial index vs. a hypothetical full index to ensure the size advantage persists.
Conclusion
Partial indexes are a lightweight tool for tailoring PostgreSQL’s indexing to the real access patterns of your data. By focusing on the subset that matters most, you reduce index size, lower maintenance costs, and often improve query latency. Use them judiciously, verify planner usage with EXPLAIN ANALYZE, and keep an eye on maintenance overhead when data moves in and out of the indexed predicate.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.