All posts
Knowledge Data Systems PostgreSQL May 12, 2026

Postgres Indexing Review: B-tree, B+ Tree, and GIN

A compact poster and bilingual review note for Postgres indexing: table versus index, B-tree and B+ tree behavior, GIN indexes, jsonb operator traps, and index selection strategy.

This is a review note for Postgres indexing. It covers B-tree, B+ tree, GIN, table/index storage, primary keys, jsonb operators, index selection, SQL syntax, trade-offs, and common interview traps.

Postgres Indexing Review: B-tree, B+ Tree, and GIN

Part 1. Table vs Index vs Primary Key

Concept Meaning Physical implementation in Postgres
Table Logical container for rows Heap: unordered row storage
Index Extra data structure that speeds up queries Stored separately from the table, such as B-tree or GIN
Primary Key Uniqueness plus not-null constraint Automatically creates a unique B-tree index

Key facts:

  • A table does not technically need a primary key, but production tables usually should have one.
  • One table can have many indexes, and the index types can be different.
  • In Postgres, the table heap and indexes are separate physical structures.
  • In MySQL InnoDB, the primary key index is the clustered table storage. This is a major difference.

中文记忆:

Postgres: 表是 Heap,索引是额外结构。
MySQL InnoDB: 主键索引就是聚簇表本身。

Primary key syntax sugar:

CREATE TABLE users (
  userid bigint PRIMARY KEY,
  name text
);

-- Conceptually similar to:
CREATE TABLE users (
  userid bigint NOT NULL,
  name text
);

ALTER TABLE users
ADD CONSTRAINT users_pkey PRIMARY KEY (userid);

-- Database automatically creates:
CREATE UNIQUE INDEX users_pkey ON users (userid);

Part 2. B-tree Index

B means Balanced, not Binary.

A B-tree index is the default Postgres index type. It is a self-balancing multi-way search tree, designed to keep lookup depth small.

Operation Complexity Compared with
Lookup O(log n) Sequential scan is O(n)
Insert/delete O(log n) Index maintenance cost is paid on writes
Range query O(log n + k) k is the number of matching rows

For 10 million rows, a full table scan may inspect 10 million rows. A B-tree lookup can reach the target in roughly a few dozen page steps, depending on fanout and caching.

Good query patterns:

WHERE id = 5
WHERE created_at >= '2026-01-01'
WHERE amount BETWEEN 100 AND 500
WHERE title LIKE 'Post%'
ORDER BY created_at

Bad query patterns for a normal B-tree:

WHERE title LIKE '%index%'     -- use pg_trgm + GIN
WHERE tags @> ARRAY['AI']     -- use GIN on array
WHERE metadata @> '{...}'     -- use GIN on jsonb

Composite index and the leftmost prefix rule:

CREATE INDEX ON events (status, created_at, region);

-- Good
WHERE status = 'active';
WHERE status = 'active' AND created_at >= '2026-01-01';
WHERE status = 'active' AND created_at >= '2026-01-01' AND region = 'us-east';

-- Not good
WHERE created_at >= '2026-01-01';

-- Partial use
WHERE status = 'active' AND region = 'us-east';

Phone book analogy:

If the index is sorted by status -> created_at -> region,
you can search by status or status + created_at.
You cannot jump directly to created_at without status.

Part 3. B+ Tree: What Databases Usually Use

Postgres, MySQL, and Oracle often call the index a B-tree index, but the database-friendly implementation is B+ tree style. B-tree is the historical umbrella term.

中文记忆:

数据库口中的 B-tree,很多时候实际在讲 B+ tree 的索引思想。
不要理解成 Binary Tree;B = Balanced。

Core B+ tree rules:

1. All data lives only in leaf nodes.
2. All keys also appear in leaf nodes.
3. Internal-node keys are guide copies, not real row data.

中文记忆:

内部节点 = 指路牌
叶子节点 = 真正存 key + data / row pointer 的地方
所有 key 在叶子层完整出现;内部 key 只是冗余导航副本

Classic B-tree versus B+ tree:

Feature Classic B-tree B+ tree
Data location Internal nodes and leaf nodes Leaf nodes only
Key location Internal and leaf nodes, usually not duplicated the same way Internal guide keys plus complete keys in leaves
Internal nodes Store key plus data Store only keys for navigation
Leaf nodes Independent Linked together
Range query May need to walk back up Scan forward through linked leaves

Correct mental picture:

                       [50]            guide key, not data
                      /    \
                  [20]      [80]       guide keys
                 /    \    /    \
            +------+ +------+ +------+ +------+
            |10|d  | |30|d  | |60|d  | |90|d  |
            |20|d  | |40|d  | |70|d  | |95|d  |
            +--<->-+ +--<->-+ +--<->-+ +------+
               linked leaf pages

Range query flow:

WHERE id BETWEEN 10 AND 60
Step 1: seek the starting leaf, O(log n)
root [50] -> left -> [20] -> left -> first leaf

Step 2: scan linked leaves, O(k)
leaf 1: 10 yes, 20 yes
leaf 2: 30 yes, 40 yes
leaf 3: 60 yes
leaf 4: 70 > 60, stop

Total: O(log n + k)

Why databases prefer B+ tree:

  • The tree is shorter because internal nodes do not store data.
  • More keys fit in each internal page, so fewer random page reads are needed.
  • Range queries are fast after the start leaf is found.
  • Ordered scans are efficient because leaves are already linked in sorted order.
  • Full index scans can walk the leaf level sequentially.

Query pattern summary:

Query pattern B-tree / B+ tree behavior
WHERE id = 5 Similar point lookup, O(log n)
BETWEEN range B+ tree shines because it scans linked leaves
ORDER BY B+ tree shines because leaf order matches index order

One-line memory hook:

Internal nodes navigate. Leaf nodes store the data.
内部节点导航,叶子节点存数据。

Part 4. GIN Index

GIN means Generalized Inverted Index.

Mental model:

B-tree: row -> whole value
GIN:    element inside the value -> rows containing that element

Example:

Table data:
row 1: {"tag":"AI"}
row 2: {"tag":"DB"}
row 3: {"tag":"AI"}

GIN inverted index:
"tag:AI" -> [row 1, row 3]
"tag:DB" -> [row 2]

Write expansion:

Insert {"a":1, "b":2}

B-tree -> 1 index entry, the whole value as one key
GIN    -> 2 inverted entries, one for a:1 and one for b:2

Supported data patterns:

Type Operators Use case
jsonb @>, ?, `? , ?&`
array @>, <@, && Tags and categories
tsvector @@ Full-text search
pg_trgm LIKE '%x%' Fuzzy or arbitrary-position text matching

Trade-off:

Dimension GIN B-tree
Containment query Very fast Often full scan
Write speed Slower: one value expands into many entries Faster
Index size Larger Smaller
Best fit Read-heavy containment search General-purpose indexing

中文记忆:

GIN 用写入性能换查询性能。
GIN 写入慢不是因为“转换表”,而是因为一个值会拆成 N 个倒排索引项。

For:

{"a":1, "b":2, "c":3}

a B-tree updates one index key, but GIN updates multiple inverted entries. The write cost grows with the number of searchable elements.

Part 5. JSONB Operator Cheatsheet

Example data:

{"source":"blog", "tag":"AI", "year":2026}
Operator Meaning Example Normal jsonb GIN index?
@> Contains metadata @> '{"tag":"AI"}' Yes
? Key exists metadata ? 'source' Yes
`? ` Any key exists `metadata ?
?& All keys exist metadata ?& array['a','b'] Yes
-> Extract jsonb metadata->'source' No
->> Extract text metadata->>'source' No

Common trap:

-- Does not use a normal jsonb GIN index
SELECT *
FROM docs
WHERE metadata->>'tag' = 'AI';

-- Uses a jsonb GIN index
SELECT *
FROM docs
WHERE metadata @> '{"tag":"AI"}';

-- Or create an expression index for the extracted text expression
CREATE INDEX ON docs ((metadata->>'tag'));

SELECT *
FROM docs
WHERE metadata->>'tag' = 'AI';

中文记忆:

@> 是 jsonb containment,GIN 能服务。
->> 已经把 jsonb 取成 text,普通 jsonb GIN 就服务不了了。

Part 6. Index Selection Decision Table

Query pattern Recommended index
id = 5 B-tree
created_at BETWEEN ... AND ... B-tree, using the B+ tree leaf-chain advantage
title LIKE 'Post%' B-tree
title LIKE '%index%' pg_trgm + GIN
ORDER BY price DESC LIMIT 10 B-tree
tags @> ARRAY['AI'] GIN on array
metadata @> '{"key":"val"}' GIN on jsonb
metadata->>'source' = 'x' Expression index
content @@ 'word' GIN on tsvector

Start from the query pattern:

equality / range / sort       -> B-tree
prefix LIKE 'Post%'          -> B-tree
contains jsonb / array        -> GIN
LIKE '%text%'                 -> pg_trgm + GIN
metadata->>'field' = 'value'  -> expression index
full-text search              -> GIN on tsvector

Part 7. SQL Cheat Sheet

The three DDL verbs:

CREATE TABLE / INDEX       -- create from nothing
ALTER TABLE                -- modify existing structure
DROP TABLE / INDEX         -- remove permanently

Common ALTER TABLE operations:

ALTER TABLE t ADD COLUMN c text;
ALTER TABLE t DROP COLUMN c;
ALTER TABLE t ALTER COLUMN c TYPE jsonb;
ALTER TABLE t RENAME COLUMN old TO new;
ALTER TABLE t RENAME TO new_name;
ALTER TABLE t ADD CONSTRAINT u UNIQUE (email);

Common index creation patterns:

-- Default B-tree
CREATE INDEX ON users (email);

-- Unique B-tree
CREATE UNIQUE INDEX ON users (email);

-- Composite B-tree
CREATE INDEX ON events (status, created_at);

-- GIN for jsonb or array
CREATE INDEX ON docs USING gin (metadata);

-- Expression index
CREATE INDEX ON docs ((metadata->>'source'));

-- Partial index
CREATE INDEX ON users (email)
WHERE deleted_at IS NULL;

Part 8. Core Trade-offs

Indexes bring:

Good: queries get faster, often O(n) -> O(log n)
Bad: writes get slower because every write must update indexes
Bad: indexes use disk space
Bad: indexes have maintenance cost, such as vacuuming or rebuilding

Practical rules of thumb:

  • Keep the number of indexes per table limited. A rough rule is <= 5, not a hard law.
  • Create indexes for frequent and important query patterns.
  • If you cannot explain which query an index serves, it may not deserve to exist.
  • A well-designed composite index can cover multiple query shapes better than many single-column indexes.

中文记忆:

索引不是越多越好。
每个索引都在用写入性能和磁盘空间换查询速度。

Part 9. Postgres vs MySQL InnoDB

Dimension Postgres MySQL InnoDB
Table storage Heap, unordered Clustered index, primary key is the table layout
jsonb support Strong, first-class Weaker
Index types B-tree, GIN, GiST, BRIN, Hash Mostly B-tree
Full-text search Built-in tsvector + GIN Weaker for this use case
Array type Native support No comparable native array type

The key difference for storage:

Postgres: table heap and indexes are separate.
InnoDB: clustered primary key index stores the table rows.

Part 10. Interview Q&A Templates

Q1. What is a B-tree?

A B-tree is a balanced, not binary, multi-way search tree. It is the default Postgres index type. It gives O(log n) lookup and is good for equality, range, and sorting queries. In practice, database B-tree indexes are B+ tree style, but people commonly call the index type B-tree.

Q2. What is the difference between B-tree and B+ tree?

Three points: first, B+ tree stores data only in leaf nodes; second, leaf nodes are linked; third, internal-node keys are guide copies used for navigation. This makes range scans and ordered scans efficient because the database can find the first leaf and then scan forward through the leaf chain.

中文答法:

B+ tree 内部节点只负责导航,真正的数据都在叶子。
范围查询找到起点后,沿叶子链表扫,不需要反复回溯上层。

Q3. When should we use GIN?

Use GIN when the query asks whether one column value contains some element, such as jsonb metadata filtering, array membership, full-text search, or trigram search. GIN is fast for reads but slower for writes and larger on disk, so it fits read-heavy containment search.

Q4. Why is GIN slower to write?

A single value is decomposed into many index entries. For example, {"a":1,"b":2} updates separate inverted entries for a:1 and b:2. The write cost scales with the number of searchable elements.

Q5. Does a table need a primary key?

Technically no. Practically, usually yes. A primary key gives unique row identity, helps ORMs and replication/CDC, and creates a unique B-tree index automatically.

Q6. Why do databases use B-tree/B+ tree instead of BST or hash?

A BST has low fanout, so it becomes too tall for disk/page storage. B-tree/B+ tree nodes hold many keys, so the tree stays shallow. Hash indexes are good for equality but not range queries or sorting. B-tree/B+ tree is more general.

Q7. How do you debug a slow query?

Start with EXPLAIN ANALYZE. Check whether the plan is doing a sequential scan. If an index exists but is not used, check whether the query shape matches the index: leftmost prefix, operator compatibility, expression indexes, and selectivity.

Part 11. Three Interview Questions for Any Index Problem

Ask these in order:

1. What is the query pattern?
   equality / range / containment / sort / fuzzy search?

2. Can the data structure serve it?
   B+ tree is good for equality, range, and sort.
   GIN is good for containment.

3. Is the cost acceptable?
   writes get slower, indexes take disk space, and maintenance increases.

中文记忆:

先看查询模式,再选数据结构,最后算代价。

Part 12. Common Mistakes

Wrong statement Correct statement
B in B-tree means Binary B means Balanced
It is a reversed index It is an inverted index
In Postgres, the table itself is a B-tree In Postgres, the table is a heap; the index is B-tree/GIN/etc.
A table must have a primary key It does not have to, but it usually should
GIN writes are slow because it uses a conversion table GIN writes are slow because one value becomes many inverted entries
metadata->>'x' = 'y' can use normal jsonb GIN It cannot; use containment or an expression index
More indexes are always better More indexes slow writes and consume storage

Thanks for reading.

© 2026 Alan Wang