PostgreSQL Expression Index

Index computed values and functions

🧮 What is an Expression Index?

An expression index indexes the result of a function or expression rather than column values directly. This speeds up queries that use functions like LOWER(), UPPER(), or calculations in WHERE clauses.


-- Index lowercase email for case-insensitive search
CREATE INDEX idx_email_lower 
ON users(LOWER(email));
                                    

Output:

CREATE INDEX

Key Expression Index Uses

🔤

Case-Insensitive

Search without case sensitivity

-- Lowercase index
CREATE INDEX idx_name_lower 
ON users(LOWER(name));
📅

Date Parts

Index year, month, or day

-- Index by year
CREATE INDEX idx_year 
ON orders(EXTRACT(YEAR FROM created_at));
🔢

Calculations

Index computed values

-- Index total price
CREATE INDEX idx_total 
ON orders((price * quantity));
📝

Text Functions

Index text transformations

-- Index substring
CREATE INDEX idx_domain 
ON emails(SUBSTRING(email FROM '@(.*)$'));

🔹 Case-Insensitive Search Index

The most common expression index uses LOWER() or UPPER() for case-insensitive searches. This allows users to search for 'john', 'John', or 'JOHN' and get the same results quickly.

-- Create users table
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255),
    name VARCHAR(100)
);

-- Create case-insensitive index
CREATE INDEX idx_email_lower ON users(LOWER(email));

-- Query must use the same function to use the index
SELECT * FROM users WHERE LOWER(email) = LOWER('[email protected]');

-- This also works
SELECT * FROM users WHERE LOWER(email) = '[email protected]';

Output:

Finds users regardless of email case

🔹 Date Part Expression Index

Index specific parts of dates like year, month, or day. This is useful for queries that filter by date components, like finding all orders from a specific year.

-- Create orders table
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER,
    amount DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT NOW()
);

-- Index by year
CREATE INDEX idx_order_year 
ON orders(EXTRACT(YEAR FROM created_at));

-- Fast query for orders by year
SELECT * FROM orders 
WHERE EXTRACT(YEAR FROM created_at) = 2024;

-- Index by month and year
CREATE INDEX idx_order_month 
ON orders(EXTRACT(YEAR FROM created_at), EXTRACT(MONTH FROM created_at));

Output:

Quickly finds orders from specific time periods

🔹 Calculation Expression Index

Index calculated values like totals, percentages, or other mathematical expressions. This speeds up queries that filter or sort by computed values without storing them separately.

-- Create order_items table
CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER,
    price DECIMAL(10,2),
    quantity INTEGER,
    discount DECIMAL(5,2) DEFAULT 0
);

-- Index total price (price * quantity)
CREATE INDEX idx_total_price 
ON order_items((price * quantity));

-- Fast query for items by total
SELECT * FROM order_items 
WHERE (price * quantity) > 100;

-- Index with discount applied
CREATE INDEX idx_final_price 
ON order_items((price * quantity * (1 - discount/100)));

Output:

Efficiently queries calculated values

🔹 JSON Expression Index

Index specific fields within JSON columns for fast queries on JSON data. This allows you to query JSON properties as efficiently as regular columns.

-- Create products table with JSON
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200),
    attributes JSONB
);

-- Index a specific JSON field
CREATE INDEX idx_product_color 
ON products((attributes->>'color'));

-- Fast query on JSON field
SELECT * FROM products 
WHERE attributes->>'color' = 'red';

-- Index nested JSON field
CREATE INDEX idx_product_price 
ON products(((attributes->'pricing'->>'amount')::NUMERIC));

Output:

Queries JSON fields as fast as regular columns

🔹 Text Pattern Expression Index

Create indexes for pattern matching and text extraction. This is useful for searching by domain names, area codes, or other text patterns within strings.

-- Create contacts table
CREATE TABLE contacts (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255),
    phone VARCHAR(20)
);

-- Index email domain
CREATE INDEX idx_email_domain 
ON contacts(SUBSTRING(email FROM '@(.*)$'));

-- Find all users from a domain
SELECT * FROM contacts 
WHERE SUBSTRING(email FROM '@(.*)$') = 'example.com';

-- Index area code from phone
CREATE INDEX idx_area_code 
ON contacts(SUBSTRING(phone FROM 1 FOR 3));

Output:

Fast searches on text patterns

🔹 Important Rules

Expression indexes require exact matching between the index expression and query expression. The query must use the same function and parameters as the index to benefit from it.

✅ Remember:

  • Query must use the EXACT same expression as the index
  • Use parentheses for complex expressions: ((price * quantity))
  • Expression indexes are immutable - they don't change when data changes
  • Test with EXPLAIN to verify index usage

Example:

-- Index created with LOWER()
CREATE INDEX idx_name ON users(LOWER(name));

-- ✅ This uses the index
SELECT * FROM users WHERE LOWER(name) = 'john';

-- ❌ This does NOT use the index
SELECT * FROM users WHERE name = 'john';

🧠 Test Your Knowledge

What does an expression index store?