Covering, Partial and Expression Indexes
Build covering indexes with INCLUDE to eliminate heap fetches, partial indexes to shrink index size for sparse queries, and expression indexes for case-insensitive search — each shown with a real query plan.
Most index advice stops at "add a B-tree and move on." This course goes one layer deeper: we look at three index shapes that each solve a specific problem B-tree alone cannot. We start with covering indexes — using INCLUDE to carry extra columns into the index leaves so a query never has to fetch the heap at all, landing an Index Only Scan. Then we build a partial index, pruning the index down to only the rows that actually matter for a query, and watch the size shrink from tens of megabytes to under three. We finish with expression indexes, where the index stores the result of a function — lower(email) — so a case-insensitive lookup hits the index instead of scanning every row. Every claim is backed by a real EXPLAIN ANALYZE, run on a live Postgres with a million orders.
What you'll build
- Build a covering index with INCLUDE and verify Index Only Scan with Heap Fetches: 0
- Explain the difference between a key column and an INCLUDE column
- Create a partial index with a WHERE predicate and measure the size reduction
- Predict when the planner will and will not use a partial index
- Build an expression index on lower(email) and get an Index Scan for case-insensitive lookup
- Measure index sizes with pg_relation_size
- Describe the read/write/storage trade-off for each index type
Contents
- A database to experiment on
- Wait for it to be ready
- One command to bring it up
- Start the server
- A short way into the database
- The customers table
- The orders table
- The tickets table
- Create the tables
- Seed the customers
- Seed the orders
- Seed the tickets
- Load the data
- Fresh statistics and a single-threaded planner
- Confirm what we are sitting on
- The query a covering index is built for
- No index, no shortcut
- A composite index on customer and date
- The index eliminates the scan, but not all heap work
- The query for Index Only Scan
- INCLUDE — carry the column without making it a key
- Refresh the visibility map
- Heap Fetches: 0
- Measure what INCLUDE costs
- The covering index is 39 MB
- The query a partial index is built for
- The plan before any index
- A full index on status for comparison
- Better, but the index carries dead weight
- A partial index with a WHERE predicate
- The planner picks the partial index
- Measure the size reduction
- 2 MB versus 38 MB
- A query that does not match the predicate
- The partial index is invisible
- The query an expression index is built for
- Seq Scan on customers
- An index on the expression, not the column
- Same query, new index
- Index Scan using customers_lower_email_idx
- When the expression does not match
- Seq Scan again — the expression did not match
- Three questions before creating an index
- Your own plan-reading tool