Partial, Expression & Generated-Column Indexes
Partial, Expression & Generated-Column Indexes
Beyond plain B-tree indexes, PostgreSQL supports three powerful index variants that let you index exactly what your queries need — nothing more, nothing less.
Expression Indexes
An expression index stores the result of an arbitrary expression rather than a raw column value. The optimizer will use it when the same expression appears in a WHERE clause.
-- Case-insensitive email lookup
CREATE UNIQUE INDEX idx_users_email_ci
ON users (lower(email));
-- Query must use the same expression to hit the index
SELECT * FROM users WHERE lower(email) = lower('Alice@Example.com');
-- Index on a JSONB extracted field
CREATE INDEX idx_orders_country
ON orders ((metadata ->> 'country'));
-- Query
SELECT * FROM orders WHERE (metadata ->> 'country') = 'DE';Partial Indexes
A partial index only includes rows that satisfy a WHERE predicate. This makes the index smaller, faster to scan, and cheaper to maintain.
-- Only index active, unverified users — the common query target
CREATE INDEX idx_users_pending_verification
ON users (email)
WHERE is_active = true AND email_verified = false;
-- Unique partial index: enforce uniqueness only for published slugs
CREATE UNIQUE INDEX idx_posts_published_slug
ON posts (slug)
WHERE published = true;
-- Partial index on a soft-delete column
CREATE INDEX idx_orders_open
ON orders (created_at)
WHERE deleted_at IS NULL;Generated Columns
A generated column's value is automatically computed from other columns. PostgreSQL only supports STORED generated columns (the value is persisted on disk). You can index a generated column just like any other.
ALTER TABLE users
ADD COLUMN email_lower text
GENERATED ALWAYS AS (lower(email)) STORED;
CREATE UNIQUE INDEX idx_users_email_lower
ON users (email_lower);
-- Another example: full name for display
ALTER TABLE users
ADD COLUMN full_name_search tsvector
GENERATED ALWAYS AS (
to_tsvector('english', coalesce(first_name, '') || ' ' || coalesce(last_name, ''))
) STORED;
CREATE INDEX idx_users_name_fts ON users USING GIN (full_name_search);Generated columns replace the pattern of maintaining a redundant computed column via application code or triggers. Because the database owns the computation, the value is always consistent and never accidentally stale.