Engineer B-tree indexes from operator semantics, sort requirements, column order, data distribution, and PostgreSQL 18 skip-scan evidence rather than left-prefix folklore.

B-Tree Structure, Operator Classes, Sort Order, Multicolumn Rules, and Skip Scan

Understand B-tree key semantics from operator classes through PostgreSQL 18 skip scan, then verify filtering and ordering behavior with repeatable EXPLAIN evidence.

Intermediate → Advanced170–220 minutesEvidence-driven index engineering labCurrent patched PostgreSQL 18.xCore B-tree/Hash/GiST/SP-GiST/GIN/BRIN; no third-party extension requiredLocal table-owner privileges; admin visibility where notedEXPLAIN ANALYZE write examples are transaction-wrapped and rolled backNo paid or managed-service dependencyLast reviewed: August 2026

Learning outcomes

ServiceHub has reached the point where “add an index” is no longer a useful performance recommendation. Its work-order table supports equality lookups, tenant-scoped time ranges, recent-first queues, and occasional searches that constrain a later column but omit the leading column. A B-tree is PostgreSQL's default index access method because it supports ordered comparisons well, but the useful unit is not “column → index.” The useful unit is query operator + sort contract + data distribution + index key order + planner evidence.

01

Explain B-tree ordering, operator classes, operator families, collation, and how they define indexable semantics.

02

Design ASC/DESC/NULLS ordering that can satisfy both filtering and ORDER BY without assuming every index scan has one direction.

03

Reason about multicolumn leading-column rules and distinguish filtering inside an index from actually narrowing the scanned index range.

04

Demonstrate PostgreSQL 18 skip scan and recognize Index Searches evidence without assuming the planner must choose it.

05

Use EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS) safely and interpret plans as local observations rather than benchmark promises.

Central rule

An index is useful only for operators that its access method/operator class knows how to support. A B-tree over a column does not magically accelerate every expression involving that column, and a multicolumn B-tree is not described accurately by the old slogan “only the leftmost prefix works.” PostgreSQL can use later columns for filtering and PostgreSQL 18 can sometimes navigate them with skip scan.

1. Build a deterministic ServiceHub index lab

The lab creates 160,000 work-order observations. tenant_id has only eight distinct values; work_order_id is unique and highly selective; timestamps are monotonic with the generated sequence. This combination lets us reason about both conventional leading-key scans and PostgreSQL 18 skip scans.

sql · setup
DROP TABLE IF EXISTS app.ch10_btree_lab;CREATE TABLE app.ch10_btree_lab (    work_order_id bigint NOT NULL,    tenant_id integer NOT NULL,    status text NOT NULL,    priority smallint NOT NULL,    created_at timestamptz NOT NULL,    due_at timestamptz);INSERT INTO app.ch10_btree_labSELECT g,       (g % 8) + 1,       CASE g % 4 WHEN 0 THEN 'done' WHEN 1 THEN 'queued' WHEN 2 THEN 'assigned' ELSE 'cancelled' END,       (g % 5) + 1,       timestamptz '2026-01-01 00:00+00' + g * interval '20 seconds',       CASE WHEN g % 11 = 0 THEN NULL            ELSE timestamptz '2026-01-01 00:00+00' + g * interval '20 seconds' + interval '2 days' ENDFROM generate_series(1,160000) AS g;ANALYZE app.ch10_btree_lab;

Use ANALYZE before comparing plans. Otherwise you are evaluating the planner with poor cardinality evidence rather than evaluating the index design.

2. B-tree semantics come from operator classes

A PostgreSQL index access method is a framework. An operator class tells that method how one data type behaves for the method's strategies. For B-tree this normally means equality and ordered comparisons. Operator families group compatible classes and can define cross-type behavior. Collation can also change text ordering, so an index's comparison semantics are not merely its column data type.

sql · inspect B-tree operator classes
SELECT am.amname,       opc.opcname,       opc.opcintype::regtype AS input_type,       opc.opcdefaultFROM pg_opclass AS opcJOIN pg_am AS am ON am.oid = opc.opcmethodWHERE am.amname = 'btree'  AND opc.opcintype IN ('text'::regtype, 'int4'::regtype, 'timestamptz'::regtype)ORDER BY input_type::text, opc.opcname;

The catalog query proves which classes are installed on this server; it does not prove which one a particular index uses. Inspect the index definition and attribute metadata too.

sql · create and inspect an ordered multicolumn index
CREATE INDEX ch10_btree_tenant_created_idxON app.ch10_btree_lab    (tenant_id ASC, created_at DESC, work_order_id ASC);SELECT pg_get_indexdef('app.ch10_btree_tenant_created_idx'::regclass);
psql · psql inspection (meta-command, not SQL)
\d+ app.ch10_btree_lab

The \d+ line is a psql meta-command, not SQL. If you automate metadata checks in an application or migration test, use catalog queries such as pg_get_indexdef() rather than parsing psql's human display.

3. Filtering and ordering can be solved by the same index

For a tenant-specific recent-work query, equality on the leading key plus an ordered second key gives a compact index range and an order that can often satisfy ORDER BY directly.

sql · tenant-scoped recent work
EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS)SELECT work_order_id, created_atFROM app.ch10_btree_labWHERE tenant_id = 3  AND created_at >= timestamptz '2026-01-20 00:00+00'ORDER BY created_at DESC, work_order_id ASCLIMIT 40;

On a typical local run, expect an Index Scan or Index Only Scan using ch10_btree_tenant_created_idx, with an Index Cond containing the tenant and timestamp constraints. Do not memorize a cost or elapsed time: cache state, hardware, statistics, visibility-map state, and PostgreSQL settings all affect the chosen plan.

A B-tree can scan forward or backward. However, mixed sort directions and NULL placement can make an explicitly ordered multicolumn index valuable when the workload requires an exact compound ordering.

sql · explicit NULL ordering contract
CREATE INDEX ch10_btree_due_idxON app.ch10_btree_lab (tenant_id, due_at ASC NULLS LAST, work_order_id);EXPLAIN (ANALYZE, BUFFERS, SETTINGS)SELECT work_order_id, due_atFROM app.ch10_btree_labWHERE tenant_id = 6ORDER BY due_at ASC NULLS LAST, work_order_idLIMIT 30;

4. Multicolumn rules: what actually narrows the scan?

For a multicolumn B-tree, equality constraints on leading columns and an inequality on the first non-equality column reliably constrain the scanned index range. Conditions on later columns can still be applied at the index level and save heap visits, but historically they did not necessarily let PostgreSQL jump over large irrelevant index regions. PostgreSQL 18 adds skip scan, which can perform repeated searches by synthesizing equality constraints for omitted earlier columns when that is cost-effective.

sql · statistics explain why a skip may be plausible
SELECT attname, n_distinct, correlationFROM pg_statsWHERE schemaname = 'app'  AND tablename = 'ch10_btree_lab'  AND attname IN ('tenant_id','work_order_id','created_at')ORDER BY attname;

The leading tenant_id has very low cardinality in this dataset. That makes “repeat one search per tenant group” potentially cheaper than reading the entire index or heap.

5. PostgreSQL 18 skip scan: observe, do not assume

sql · index designed to expose skip-scan opportunity
CREATE INDEX ch10_btree_skip_idxON app.ch10_btree_lab (tenant_id, work_order_id);ANALYZE app.ch10_btree_lab;EXPLAIN (ANALYZE, BUFFERS, SETTINGS)SELECT tenant_id, work_order_idFROM app.ch10_btree_labWHERE work_order_id = 98765;

On PostgreSQL 18, this query is a strong skip-scan candidate because the predicate is on the second index column and the first column has only eight values. When skip scan is used, EXPLAIN ANALYZE can report an Index Searches count greater than one. The node is still an index/index-only scan; there is no requirement for a plan node literally named “Skip Scan.”

Evidence boundary

If your run chooses another competing index or a sequential scan instead, PostgreSQL is not “failing to support skip scan.” It is making a cost-based choice from the indexes and statistics available. To study the feature, remove competing indexes only in a disposable lab or construct a table where the later-column lookup and low-cardinality leading column make the candidate unambiguous.

Skip scan does not convert every poor column order into a good design. If the omitted leading key has thousands or millions of distinct values, repeated searches may approach scanning the whole index, and a dedicated later-column index can be a better portfolio choice.

6. Wrong approach: one wide index for every query

A learner may react to skip scan by creating (tenant_id,status,priority,created_at,work_order_id) and expecting it to replace all narrower indexes. That can increase storage and write work, hurt HOT opportunities when indexed columns are updated, and still fail to match the workload's operators/orderings.

sql · measure the physical cost of candidate indexes
SELECT indexrelid::regclass AS index_name,       pg_size_pretty(pg_relation_size(indexrelid)) AS size,       idx_scan, idx_tup_read, idx_tup_fetchFROM pg_stat_user_indexesWHERE relid = 'app.ch10_btree_lab'::regclassORDER BY pg_relation_size(indexrelid) DESC;

idx_scan = 0 is not a drop command. Statistics can have been reset, a reporting cycle may not have run yet, replicas may serve reads, and constraint indexes may protect correctness even if scans are rare. Lesson 5 turns these signals into an audit discipline.

7. Production judgment and cleanup

Choose B-tree key order from the highest-value query shapes, not from table-column order. Verify operator class/collation semantics, cardinality of leading keys, sort requirements, selectivity, update frequency, visibility conditions for index-only scans, and competing indexes. PostgreSQL 18 skip scan is an optimization opportunity—not a schema-design exemption.

sql · cleanup
DROP TABLE IF EXISTS app.ch10_btree_lab;

Check your understanding

  1. What does an operator class contribute to an index?
  2. Why can a later B-tree key still be useful even when it does not conventionally narrow the initial index range?
  3. What PostgreSQL 18 EXPLAIN field can reveal repeated B-tree searches associated with skip scan?
  4. Why does low cardinality in the skipped leading column make skip scan more attractive?
  5. Why is a chosen sequential scan not proof that an existing index is broken?
Review the answers

The operator class defines the operators/support semantics an access method can apply to a data type. Later keys can be checked inside the index and, in PostgreSQL 18, may also participate in skip-scan navigation. EXPLAIN ANALYZE can show Index Searches. A small number of leading-key groups makes repeated searches cheap enough to skip large irrelevant portions. Planner choice is cost-based, so a sequential scan can be correct even when an index exists.

Authoritative references

Index behavior depends on PostgreSQL major version, operator class, collation, statistics, data distribution, visibility, and workload. Use the documentation for the exact major you operate.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.