Supabase’s query optimizer is surprisingly bad at automatically creating indexes for your tables, leaving you to do the heavy lifting.

Let’s say you have a users table with a last_login timestamp and you want to find all users who logged in recently. A naive query might look like this:

SELECT * FROM users WHERE last_login > NOW() - INTERVAL '1 day';

Without an index on last_login, Supabase has to scan the entire users table, which can be painfully slow if you have millions of users.

Here’s the same query after we’ve added an index:

-- First, create the index
CREATE INDEX idx_users_last_login ON users (last_login);

-- Then, run the query
SELECT * FROM users WHERE last_login > NOW() - INTERVAL '1 day';

Now, Supabase can use the idx_users_last_login index to quickly locate the rows where last_login meets your criteria, dramatically speeding up the query.

The Problem: Missing Indexes

The core issue is that Supabase, like many PostgreSQL-based systems, relies on you to tell it which columns are frequently used in WHERE clauses, JOIN conditions, or ORDER BY clauses. It doesn’t magically know that last_login in our example is a good candidate for an index.

How Indexes Work (The Short Version)

Think of an index as the index at the back of a book. Instead of reading every page to find a topic, you look up the topic in the index, which tells you exactly which pages to go to. In a database, an index is a separate data structure (often a B-tree) that stores a sorted copy of the indexed column(s) along with pointers to the actual table rows. When you query data based on an indexed column, the database can traverse the index much faster than it can scan the entire table.

Identifying Slow Queries

The first step to optimization is finding out what is slow. Supabase provides tools for this:

  1. EXPLAIN ANALYZE: This is your best friend. Prefix any slow query with EXPLAIN ANALYZE to see the query plan the database is using and the actual execution time.

    EXPLAIN ANALYZE SELECT * FROM users WHERE last_login > NOW() - INTERVAL '1 day';
    

    Look for Seq Scan (Sequential Scan) on large tables. This means the database is reading every row. You want to see Index Scan or Bitmap Heap Scan using an index you expect.

  2. Supabase Dashboard: Navigate to Database -> Reports -> Slow Queries. This will show you queries that have exceeded a certain threshold (e.g., 500ms) and their execution counts.

Common Indexing Scenarios and Solutions

  • Filtering by a specific column (WHERE): This is the most common use case. If you frequently filter by users.email or products.category, index those columns.

    -- Diagnosis:
    EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
    -- Look for Seq Scan on 'users' table.
    
    -- Fix:
    CREATE INDEX idx_users_email ON users (email);
    -- Why it works: Allows the database to jump directly to rows where email matches, bypassing a full table scan.
    
  • Joining tables (JOIN): When joining orders to users on orders.user_id = users.id, if orders.user_id is not indexed, the join will be slow.

    -- Diagnosis:
    EXPLAIN ANALYZE SELECT o.* FROM orders o JOIN users u ON o.user_id = u.id WHERE u.username = 'johndoe';
    -- Look for Seq Scan on 'orders' table, especially during the join phase.
    
    -- Fix:
    CREATE INDEX idx_orders_user_id ON orders (user_id);
    -- Why it works: Speeds up finding matching user IDs in the 'orders' table for each user.
    
  • Sorting results (ORDER BY): If you often sort by created_at in descending order, an index can help.

    -- Diagnosis:
    EXPLAIN ANALYZE SELECT * FROM posts ORDER BY created_at DESC;
    -- Look for "Sort" operations that are costly.
    
    -- Fix:
    CREATE INDEX idx_posts_created_at ON posts (created_at DESC);
    -- Why it works: The index is already sorted, so the database can read the results in the desired order directly from the index.
    
  • Range queries (>, <, BETWEEN): As seen in our last_login example, range queries benefit greatly from indexes.

    -- Diagnosis:
    EXPLAIN ANALYZE SELECT * FROM events WHERE event_timestamp BETWEEN '2023-10-26 00:00:00' AND '2023-10-27 00:00:00';
    -- Look for Seq Scan on 'events' table.
    
    -- Fix:
    CREATE INDEX idx_events_event_timestamp ON events (event_timestamp);
    -- Why it works: The index allows the database to efficiently find the start and end points of the timestamp range and retrieve matching rows.
    
  • Composite Indexes: Sometimes, you query on multiple columns together. A composite index on (column1, column2) can be more efficient than separate indexes if the columns are always used together in the same order.

    -- Diagnosis:
    EXPLAIN ANALYZE SELECT * FROM messages WHERE sender_id = 123 AND recipient_id = 456 ORDER BY created_at;
    -- Look for Seq Scan or inefficient filtering on multiple columns.
    
    -- Fix:
    CREATE INDEX idx_messages_sender_recipient_created ON messages (sender_id, recipient_id, created_at);
    -- Why it works: This index can satisfy all parts of the WHERE clause and the ORDER BY clause efficiently, as it's sorted by sender_id, then recipient_id, then created_at.
    
  • pg_stat_user_indexes View: This built-in PostgreSQL view can show you how often indexes are being used.

    SELECT
        schemaname,
        relname,
        indexrelname,
        idx_scan,
        idx_tup_read,
        idx_tup_fetch
    FROM
        pg_stat_user_indexes
    WHERE
        schemaname = 'public' -- Or your specific schema
    ORDER BY
        idx_scan DESC;
    

    Indexes with idx_scan of 0 (or very low) might be candidates for removal if they aren’t being used. However, be cautious; an index might be used only for infrequent but critical queries.

The Subtle Nuance: Index Selectivity

Not all indexes are created equal. An index on a column with very few unique values (e.g., a boolean is_active column where 99% of users are active) is often less effective than an index on a highly unique column (like email). The database’s query planner considers "selectivity" – how many rows an index is likely to return. For very low-selectivity columns, a sequential scan might sometimes be faster than using the index, as traversing the index and then fetching rows can be more overhead. EXPLAIN ANALYZE will help you see if your index is actually being used effectively.

The next hurdle after indexing is often understanding how to deal with large data volumes using partitioning.

Want structured learning?

Take the full Supabase course →