Partial indexing is a powerful technique to optimize database performance, reduce index bloat, and slash IOPS by indexing only the rows you actually query.
My pager went off at 3 AM last Tuesday because our primary PostgreSQL instance hit 98% disk utilization. The culprit wasn't a sudden surge in traffic, but a bloated index on our orders table that had grown to nearly 40GB. We were indexing every single row, including millions of archived records that hadn't been touched in three years.
I realized then that we were paying a heavy tax for "just-in-case" indexing. By shifting to partial indexing, we didn't just save disk space; we significantly improved our query performance.
In most systems, we default to indexing columns like status or created_at across the entire table. If you're building a dashboard that only cares about active orders, why are you keeping the cancelled or archived states in your B-tree? Every time you insert a new record, the database has to update that massive index structure, which increases write latency and inflates your IOPS.
When I started cleaning up our schema, I saw that our idx_orders_status was scanning through 15 million rows just to find the 200,000 active ones. That’s a massive waste of memory and CPU cycles.
A partial index is simply an index with a WHERE clause. It only contains entries for rows that satisfy the predicate.
Here is how we refactored our orders index:
SQL-- The old, bloated way CREATE INDEX idx_orders_status_all ON orders (status); -- The new, efficient way CREATE INDEX idx_orders_active ON orders (id, customer_id) WHERE status = 'active';
By adding id and customer_id into the index, we created a covering index for our most common lookup. Because the index is now significantly smaller—roughly 1/10th the size of the original—it fits entirely in RAM. This effectively eliminates the need for heap fetches, which is the gold standard for covering indexes.
Partial indexing isn't a silver bullet. You’ll run into trouble if your application queries change. If a developer suddenly adds a feature to search for archived orders, the query planner will ignore your partial index, perform a sequential scan, and your latency will spike.
We also learned the hard way that partial indexes don't work for global uniqueness. If you need to enforce a unique constraint, you're stuck with a standard index. For everything else, though, the storage efficiency is worth the extra maintenance.
If you are dealing with high-cardinality data, you should check out my previous notes on partial indexes for high-cardinality filtering to see how this approach scales when you have millions of unique values.
After deploying the partial index, I monitored the pg_stat_user_indexes view. The number of index scans jumped, while the average scan time dropped from around 120ms to under 15ms.
Here is what I look for to verify the impact:
pg_size_pretty(pg_relation_size('idx_orders_active')).EXPLAIN ANALYZE to ensure the planner is actually picking your new partial index.If you're still seeing high IOPS despite these changes, you might also be suffering from table bloat or inefficient fill factors. In those cases, looking into index-organized tables might be the next logical step to physically group your data.
Can I use partial indexes for multi-column queries?
Yes. You can include any number of columns in the index. Just ensure the columns you include are the ones you actually select in your WHERE or SELECT clauses.
What happens if I forget the WHERE clause in my query? The database will ignore your partial index and likely fall back to a sequential scan. Your queries will still work, but they will be significantly slower.
Does this help with writes?
Absolutely. Since the index only tracks a subset of data, the B-tree height stays lower, and index maintenance during INSERT or UPDATE operations becomes much cheaper.
I’m still experimenting with how partial indexes interact with vacuuming frequency in PostgreSQL 16. While I’ve seen a massive boost in read performance, I’m curious if we’ll see long-term fragmentation differences in the index pages compared to full indexes. For now, the storage savings and the reduction in query latency have bought us enough breathing room to focus on other parts of our infrastructure. Don't index what you don't query.
Master an indexing strategy for app developers to fix slow production queries. Learn how to read EXPLAIN plans, pick the right columns, and avoid overhead.