Multi-Column Indexes in PostgreSQL: Design, Usage, and Performance

Learning Objectives: By the end of this lesson, you will be able to:

  • Explain how multi-column (composite) indexes work in PostgreSQL
  • Design effective multi-column indexes for different query patterns
  • Analyze and optimize queries using multi-column indexes

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Have you ever written a query with multiple conditions in the WHERE clause and wondered how the database decides which index to use? What do you think happens if you change the order of columns in an index?

Lesson Overview

  • Duration: 90 minutes
  • Prerequisites: Understanding of single-column indexes, basic SQL and PostgreSQL knowledge
  • Materials Needed: PostgreSQL instance, SQL client (psql, DBeaver, pgAdmin, etc.)

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Multi-column index structure and how column order affects usage
  2. When and why to use multi-column indexes
  3. Designing and testing multi-column indexes for real queries
  4. Hands-on practice and exercises
  5. Common pitfalls and how to avoid them

Success Criteria:

By the end of this session, you should be able to design, create, and analyze multi-column indexes for a variety of query patterns, and understand their impact on performance.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

What is a Multi-Column Index?

A multi-column (or composite) index is an index that includes more than one column. It allows PostgreSQL to efficiently search for rows based on multiple columns, especially when queries filter or sort on those columns together.

Real-World Analogy: Think of a phone book sorted by last name, then first name. If you know both, you can find someone quickly. If you only know the first name, it’s much less helpful.

Why This Matters: Many real-world queries filter on more than one column. A well-designed multi-column index can dramatically speed up such queries, but a poorly designed one can be useless.

How Column Order Matters

The order of columns in a multi-column index is critical. PostgreSQL can only use the index efficiently if the query filters on the leading (leftmost) columns.

  • Index on (A, B): Efficient for WHERE A = … or WHERE A = … AND B = …
  • Not efficient for WHERE B = … alone

Key Terminology

  • Composite Index: An index on two or more columns.
  • Prefix: The leftmost column(s) of a multi-column index.
  • Index Scan: Using an index to find matching rows.

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Multi-Column Indexes in PostgreSQL

Multi-column indexes are most useful when queries filter on multiple columns together, or when sorting by multiple columns. The order of columns in the index should match the most common query patterns.

Practical Applications

  • Searching for orders by customer_id and order_date
  • Filtering users by status and signup_date
  • Sorting products by category and price

Common Misconceptions

  1. Misconception: PostgreSQL can use any part of a multi-column index for any query. Reality: Only the leftmost columns (prefix) are used unless the query filters on all preceding columns.

  2. Misconception: The order of columns in the index doesn’t matter. Reality: The order is crucial for index usability and performance.

5. Live Coding Demonstration (20-25 minutes)

Example 1: Creating and Using a Multi-Column Index

-- Create a sample table
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INT NOT NULL,
    order_date DATE NOT NULL,
    total NUMERIC(10,2) NOT NULL
);

-- Insert sample data
INSERT INTO orders (customer_id, order_date, total)
SELECT (random()*1000)::int, NOW()::date - (random()*365)::int, (random()*500)::numeric(10,2)
FROM generate_series(1, 100000) AS g;

-- Create a multi-column index
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);

-- Query using both columns
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 AND order_date = '2024-05-01';

Explanation:

  • The index is used efficiently when both columns are filtered.

Example 2: Column Order Impact

-- Query filtering only on order_date
EXPLAIN ANALYZE SELECT * FROM orders WHERE order_date = '2024-05-01';

-- Query filtering only on customer_id
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123;

Key Points:

  • The index on (customer_id, order_date) is not used for queries filtering only on order_date.
  • It is used for queries filtering on customer_id, or both customer_id and order_date.

Example 3: Multi-Column Index for Sorting

-- Query with ORDER BY
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123 ORDER BY order_date DESC;

Key Points:

  • The index can help with both filtering and sorting if the column order matches the query.

Debugging Demonstration

Show a common mistake: creating the index in the wrong order.

-- Index on (order_date, customer_id)
CREATE INDEX idx_orders_date_customer ON orders(order_date, customer_id);

-- Query filtering on customer_id only
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123;
-- The index is not used efficiently.

6. Hands-On Practice (30-40 minutes)

Exercise 1: Basic Practice (10 minutes)

Objective: Create and use a multi-column index for a two-column query.

Instructions:

  1. Create a table called logins with columns user_id, login_time, and ip_address.
  2. Insert 10,000 sample logins.
  3. Create a multi-column index on (user_id, login_time).
  4. Query for all logins by a specific user on a specific date and compare performance with and without the index.

Starter Code:

CREATE TABLE logins (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,
    login_time TIMESTAMP NOT NULL,
    ip_address TEXT NOT NULL
);

INSERT INTO logins (user_id, login_time, ip_address)
SELECT (random()*1000)::int, NOW() - (g || ' minutes')::interval, '192.168.1.' || (random()*255)::int
FROM generate_series(1, 10000) AS g;

Solution:

CREATE INDEX idx_logins_user_time ON logins(user_id, login_time);
EXPLAIN ANALYZE SELECT * FROM logins WHERE user_id = 42 AND login_time::date = CURRENT_DATE;

Exercise 2: Intermediate Challenge (15 minutes)

Objective: Optimize a query with multiple conditions and sorting.

Requirements:

  • Create a multi-column index that supports filtering by user_id and sorting by login_time.
  • Test queries with different filter and sort combinations.
  • Observe the query plan and performance.

Hints:

  • Use EXPLAIN ANALYZE to see if the index is used.
  • Try changing the order of columns in the index and see the effect.

Exercise 3: Creative Application (15 minutes)

Objective: Design an index strategy for a reporting query.

Challenge: Suppose you need to generate a report of all logins from a specific IP address within a date range, ordered by user_id. What indexes would you create? Test your strategy and measure performance.

Extension Ideas:

  • Try adding a multi-column index on (ip_address, login_time, user_id).
  • Experiment with different column orders and see which is most effective for your queries.

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Creating a multi-column index in the wrong column order. Cause: The index is not used efficiently for common queries. Solution: Analyze your most frequent queries and match the index order to the filter/sort order.

  2. Problem: Assuming PostgreSQL can use any part of a multi-column index. Cause: Only the leftmost columns (prefix) are used unless all preceding columns are filtered. Solution: Design indexes based on query patterns, not just column combinations.

Debugging Tips

  • Use EXPLAIN and EXPLAIN ANALYZE to check index usage.
  • Try different index orders and compare query plans.
  • Use pg_stat_user_indexes to monitor index effectiveness.

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Match index column order to your most common query patterns.
  • Avoid unnecessary multi-column indexes; each adds write overhead.
  • Regularly review and drop unused indexes.

Next Steps in Your Learning Journey

  • Learn about partial and expression indexes
  • Explore covering indexes with the INCLUDE clause
  • Practice analyzing and tuning real-world queries

9. Summary and Review (5 minutes)

Key Takeaways

  1. Multi-column indexes can dramatically speed up queries with multiple conditions.
  2. The order of columns in the index is critical for performance.
  3. Always analyze and test your indexes with real queries.

Quick Review Questions

  1. When should you use a multi-column index?
  2. How does column order affect index usage?
  3. How can you verify if a multi-column index is being used by a query?

For Instructors

Teaching Tips

  • Use live demos to show the impact of column order on index usage.
  • Encourage students to experiment with different index strategies.
  • Discuss trade-offs between read and write performance.

Assessment Ideas

  • Quiz: Identify which queries would benefit from a multi-column index.
  • Project: Optimize a sample database for a set of queries with multiple conditions.
  • Peer review: Analyze and critique each other’s index strategies.

Adaptation Notes

  • For beginners, focus on two-column indexes and simple queries.
  • For advanced students, introduce three-column indexes and complex query patterns.
  • In online settings, use shared SQL sandboxes or cloud PostgreSQL instances for exercises.