PostgreSQL index access methods
Choose among heap tables, B-tree, Hash, GIN, GiST, SP-GiST, BRIN, Bloom, HNSW, and IVFFlat with clear operator and extension boundaries
PostgreSQL does not use the everyday MySQL model of selecting InnoDB or MyISAM for ordinary tables. Almost every table uses the core heap table access method. Separate index access methods and operator classes determine which queries an index supports.
A table access method is not a routine tuning switch
The PostgreSQL Table Access Method API lets extensions or custom builds implement table storage, but ordinary applications still default to heap:
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
occurred_at timestamptz NOT NULL,
payload jsonb NOT NULL
) USING heap;USING heap is normally omitted. A new table access method enters the critical path for WAL, MVCC, VACUUM, backup, replication, extensions, and major upgrades. It is not a query hint that can be switched casually.
Inspect access methods exposed by an instance:
SELECT
amname,
CASE amtype
WHEN 't' THEN 'table'
WHEN 'i' THEN 'index'
ELSE amtype::text
END AS access_method_type
FROM pg_am
ORDER BY amtype, amname;Core index access methods
PostgreSQL 18 core provides B-tree, Hash, GiST, SP-GiST, GIN, and BRIN. bloom ships as a module but requires CREATE EXTENSION bloom. Use the official Index Types as the behavior boundary.
| Type | Prefer for | Critical boundary |
|---|---|---|
| B-tree | Equality, ranges, ordering, uniqueness, anchored patterns | Default choice; column order and operator class determine usable queries |
| Hash | Single-column equality | Supports only =; B-tree is usually more versatile, so require measured benefit |
| GIN | JSONB, arrays, full text, and multi-valued content | Higher update/build cost; behavior depends on the operator class |
| GiST | Ranges, geometry, PostGIS, and nearest-neighbor search | An extensible framework, not one algorithm; operators/classes must match |
| SP-GiST | Tries, quadtrees, k-d trees, and partitioned search spaces | Fits naturally partitionable data; not a general GiST replacement |
| BRIN | Very large append-heavy tables correlated with physical order | Stores block-range summaries; weak correlation reads many heap blocks |
| Bloom extension | Equality across arbitrary combinations of many columns | Lossy and rechecked; no range, unique, or NULL search; bundled operator classes cover only int4 and text |
Operator class matters more than the index label
GIN, GiST, SP-GiST, and BRIN are frameworks. The operator class determines supported operators, ordering, and data types. “Uses GIN” is not enough information to reproduce an index design.
Common workload map
Equality / range / order / unique → B-tree
JSONB contains / array member → GIN
PostgreSQL full-text search → GIN in most cases
Range / GIS / nearest neighbor → GiST or a matching SP-GiST class
Huge, time-correlated append table → BRIN
Vector approximate-nearest-neighbor → pgvector HNSW / IVFFlatHNSW and IVFFlat come from pgvector; they are not core PostgreSQL index methods. Test recall, filtering, memory, build time, WAL, replica lag, and extension upgrades independently.
Built-in full-text search includes parsers, dictionaries, ranking, highlighting, and GIN/GiST indexing. Built-in configurations do not solve tokenization for every language. Chinese, for example, normally needs an additional tokenizer/extension or application preprocessing; creating a GIN index alone does not prove search quality.
Match the query before creating an index
-- Ordinary filter and order
CREATE INDEX CONCURRENTLY orders_customer_time_idx
ON orders (customer_id, placed_at DESC);
-- JSONB containment: payload @> '{"status":"paid"}'
CREATE INDEX CONCURRENTLY events_payload_gin_idx
ON events USING gin (payload jsonb_path_ops);
-- Large table whose timestamps correlate with physical append order
CREATE INDEX CONCURRENTLY events_time_brin_idx
ON events USING brin (occurred_at);jsonb_path_ops is focused on containment and jsonpath operators such as @>, @?, and @@; it does not support every operator available from the default jsonb_ops. Derive index DDL from real query shapes.
Record plans and size after creation:
SELECT
indexrelname,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE relname = 'events'
ORDER BY pg_relation_size(indexrelid) DESC;Then use EXPLAIN (ANALYZE, BUFFERS) to compare actual rows, heap blocks, rechecks, sorting, and write cost. See Indexes and EXPLAIN for the complete workflow.
Prefer native capability first
| Need | Validate in PostgreSQL first | Evaluate only when insufficient |
|---|---|---|
| Fuzzy search | FTS, pg_trgm contrib, expression/GIN/GiST indexes | External search or a BM25 extension |
| Work claiming | Transactions, FOR UPDATE SKIP LOCKED, advisory locks | Dedicated queue and workflow systems |
| Time lifecycle | Native partitions, BRIN, scheduled cleanup | pg_partman or TimescaleDB |
| Cross-database access | postgres_fdw, logical replication | CDC platform or separate sync system |
| Analytics | Materialized views, partitioning, parallel query | pg_duckdb, pg_mooncake, or a warehouse |
| Vector search | No core vector type or ANN index | pgvector or a dedicated vector system |
Native-first does not reject extensions. It avoids unnecessary binary, license, backup, and upgrade dependencies. See PostgreSQL extension selection for candidates.
Last updated on