Skip to content

Case 10: Optimizing a Large WHERE IN (...) Clause

The Story

  • Table: products, a large product catalog with 10,000,000 rows. The id column is the primary key and is indexed.
  • The Problem: Another microservice (like a recommendation engine) has generated a list of 15,000 product_ids that need to be displayed to a user immediately. The application receives this list of IDs and needs to fetch their details (name, price, image_url) from the database.

The Problematic SQL Query

The simplest and most direct approach is to build a giant IN clause.

sql
EXPLAIN ANALYZE
SELECT id, name, price, image_url
FROM products
WHERE id IN (
    123, 456, 789, ... -- a very long list of 15,000 IDs
);

The Bottleneck

While this query will work, it has several hidden performance problems when the IN list gets very large:

  1. Query Parsing Overhead: Sending a huge SQL string (hundreds of kilobytes or even megabytes) over the network to the database is inefficient. The database server also has to spend a significant amount of time and CPU just to parse this giant statement before it can even start planning the query.
  2. Sub-optimal Execution Plan: PostgreSQL often transforms a large IN clause into a massive tree of OR conditions (id = 123 OR id = 456 OR ...). With a very long list, the query planner can struggle to create the most efficient plan compared to more fundamental operations like a JOIN.
  3. Memory and Parameter Limits: Some database drivers and systems have limits on the length of a SQL statement or the number of parameters you can pass, which could cause the query to fail entirely.

The Solution: JOINing with a Dataset

  • The Logic: Instead of treating the 15,000 IDs as part of the query text, treat them as data. Then, perform an efficient JOIN between the products table and this new dataset of IDs.

  • Method 1: Use a VALUES Clause (Good for moderately large lists)

    This method lets you create a "virtual table" right inside your query.

    sql
    EXPLAIN ANALYZE
    SELECT p.id, p.name, p.price, p.image_url
    FROM products p
    -- Join with a virtual table created from VALUES
    JOIN (VALUES (123), (456), (789), ... ) AS id_list(id)
    ON p.id = id_list.id;

    The query planner can often handle this JOIN more efficiently than a giant IN list.

  • Method 2: Use a Temporary Table (The Most Robust and Scalable Solution)

    This is the most powerful and scalable approach.

    sql
    -- Step 1: Create a temporary table that only exists for this session.
    CREATE TEMP TABLE product_ids_to_fetch (id INT PRIMARY KEY);
    
    -- Step 2: Load the 15,000 IDs into the temp table.
    -- (In a real application, you would use your driver's COPY or bulk insert feature for this, which is extremely fast).
    INSERT INTO product_ids_to_fetch (id) VALUES (123), (456), ...;
    ANALYZE product_ids_to_fetch; -- IMPORTANT: Tell the planner about the temp table's stats.
    
    -- Step 3: Perform a highly efficient JOIN.
    EXPLAIN ANALYZE
    SELECT p.id, p.name, p.price, p.image_url
    FROM products p
    JOIN product_ids_to_fetch pi ON p.id = pi.id;

Analyzing the Result (with Method 2)

  1. Faster Planning: The final SELECT statement is tiny and simple. The planner doesn't waste time parsing a long list.
  2. Efficient Execution Plan: The execution plan will be a Hash Join or a Nested Loop Join between the two tables. These are operations that databases are highly optimized to perform. The planner has full statistics about the temporary table (after ANALYZE) to make the best choice.
  3. Bypasses Limits: You can load millions of IDs into a temporary table without running into issues with SQL statement length limits.

Conclusion

  • A WHERE IN (...) clause is convenient and efficient for small lists (a few dozen to a few hundred IDs).
  • When the list grows to thousands or more, WHERE IN becomes an anti-pattern.
  • The best practice is to turn the list of IDs into a dataset (using VALUES or a Temporary Table) and then use a JOIN. This is a common and powerful pattern for handling batch operations.