"PostgreSQL 18 Skip Scan: When Your Composite Index Can Skip the Leading Column"
postgresql 18 skip scan — how PG18's B-tree skip scan uses a composite index without the leading column, when it fires, and when to build a dedicated index instead.
The old rule was absolute: a composite index on (a, b) is useless for a query that only filters on b — the "leftmost prefix" rule. PostgreSQL 18 breaks it. With B-tree skip scan , the planner can now use that composite index for WHERE b = $1 by iterating over the distinct values of a and probing each one. Postgresql 18 skip scan doesn't fire for every query, though — it depends on the leading column's cardinality, fresh statistics, and the shape of your filter. This guide shows when it kicks in, how to confirm it in EXPLAIN , and when a dedicated index on (b) is still the right call. Last updated: October 4, 2026 What you'll learn What skip scan does to a composite B-tree index When Postgres uses skip scan: the low-cardinality leading column rule How to check for skip scan in EXPLAIN output ("Index Searches: N") Skip scan vs a dedicated index: the decision rule Why Postgres isn't using your composite index (troubleshooting) The Problem: Composite Index Without the Leading Column Consider the multi-tenant classic: CREATE TABLE events ( id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY, tenant_id integer NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), kind text NOT NULL, payload jsonb NOT NULL ); CREATE INDEX events_tenant_created_idx ON events (tenant_id, created_at); This index is perfect for WHERE tenant_id = $1 AND created_at > $2 . But an analytics query that filters only on the second column: SELECT kind, count(*) FROM events WHERE created_at > now() - interval '7 days' GROUP BY kind; On PostgreSQL 17 and earlier, the planner looks at events_tenant_created_idx , sees no constraint on the leading column tenant_id , and falls back to a sequential scan (or an index-only scan of the whole index). The standard advice was "build a…