🔥 0
0
Lesson 2 of 5 20 min +200 XP

Index Types: B-tree, Hash, GIN, GiST

B-tree isn't your only option. Different query patterns call for different index types. Choose the wrong one and your index might not be used at all.

Index Type Overview

Type Best For Supports Example
B-tree General purpose =, <, >, <=, >=, BETWEEN WHERE age > 25
Hash Equality only = only WHERE id = 123
GIN Arrays, full-text, JSONB @>, &&, to_tsvector WHERE tags @> '{sql}'
GiST Geometric, ranges <<, >>, &&, @> WHERE location <@> point

B-tree: The Default Workhorse

B-tree is the default in PostgreSQL and MySQL. It handles most use cases well.

-- B-tree is the default
CREATE INDEX idx_users_email ON users(email);

-- Explicitly specifying (same result)
CREATE INDEX idx_users_email ON users USING btree(email);

When B-tree Shines

Equality

WHERE email = 'john@test.com'

Ranges

WHERE created_at > '2024-01-01'

Sorting

ORDER BY created_at DESC

Prefix Search

WHERE name LIKE 'John%'

B-tree Limitations

-- B-tree CANNOT help with these patterns:

-- Suffix or infix search
WHERE email LIKE '%@gmail.com'  -- Scans all rows

-- Case-insensitive without proper setup
WHERE LOWER(email) = 'john@test.com'  -- Scans all rows (need functional index)

-- NOT conditions often skip index
WHERE status != 'deleted'  -- Usually scans all rows

Hash Indexes

Hash indexes use a hash table for O(1) lookups. Only for equality checks.

-- Create a hash index
CREATE INDEX idx_users_uuid ON users USING hash(uuid);

Hash vs B-tree Performance

Operation B-tree Hash
WHERE uuid = '...' O(log n) O(1)
WHERE id > 1000 Supported Not supported
ORDER BY id Supported Not supported
Index size Larger Smaller
Historical Note

Before PostgreSQL 10, hash indexes weren't WAL-logged and couldn't survive crashes. They're now safe to use, but B-tree is still usually preferred for its versatility.

GIN: Inverted Indexes

GIN (Generalized Inverted Index) is perfect for composite values - arrays, JSONB, and full-text search.

How GIN Works

GIN creates an entry for each element pointing to all rows containing it:

Traditional index (B-tree):
Row 1 → [tag1, tag2, tag3]
Row 2 → [tag1, tag4]
Row 3 → [tag2, tag3]

GIN (inverted):
tag1 → [Row 1, Row 2]
tag2 → [Row 1, Row 3]
tag3 → [Row 1, Row 3]
tag4 → [Row 2]

GIN for Arrays

-- Table with array column
CREATE TABLE articles (
  id SERIAL PRIMARY KEY,
  title TEXT,
  tags TEXT[]
);

-- GIN index on array
CREATE INDEX idx_articles_tags ON articles USING gin(tags);

-- Query: Find articles with 'sql' tag
SELECT * FROM articles WHERE tags @> ARRAY['sql'];

-- Query: Find articles with ANY of these tags
SELECT * FROM articles WHERE tags && ARRAY['sql', 'postgres'];

GIN for Full-Text Search

-- Full-text search with GIN
CREATE INDEX idx_articles_fts ON articles
  USING gin(to_tsvector('english', title || ' ' || body));

-- Query
SELECT * FROM articles
WHERE to_tsvector('english', title || ' ' || body) @@ to_tsquery('database & indexing');

GIN for JSONB

-- Index JSONB column
CREATE INDEX idx_products_data ON products USING gin(metadata);

-- Query: Find products with specific attribute
SELECT * FROM products WHERE metadata @> '{"color": "red"}';

-- Query: Check if key exists
SELECT * FROM products WHERE metadata ? 'color';
GIN Trade-offs
  • Pros: Fast reads for containment queries, perfect for arrays/JSONB
  • Cons: Slow to build, slow to update, larger than B-tree
  • Best for: Read-heavy workloads with infrequent writes

GiST: Geometric and Range Data

GiST (Generalized Search Tree) handles data that doesn't fit neatly into a linear order.

Geometric Queries with PostGIS

-- Enable PostGIS extension
CREATE EXTENSION postgis;

-- Table with geographic point
CREATE TABLE stores (
  id SERIAL PRIMARY KEY,
  name TEXT,
  location GEOGRAPHY(POINT)
);

-- GiST index for spatial queries
CREATE INDEX idx_stores_location ON stores USING gist(location);

-- Find stores within 5km of a point
SELECT name, ST_Distance(location, ST_MakePoint(-122.4194, 37.7749)::geography) as distance
FROM stores
WHERE ST_DWithin(location, ST_MakePoint(-122.4194, 37.7749)::geography, 5000)
ORDER BY distance;

Range Types

-- Table with date range
CREATE TABLE reservations (
  id SERIAL PRIMARY KEY,
  room_id INT,
  during DATERANGE
);

-- GiST index for range queries
CREATE INDEX idx_reservations_during ON reservations USING gist(during);

-- Find overlapping reservations
SELECT * FROM reservations
WHERE during && daterange('2024-03-15', '2024-03-20');

-- Check for conflicts
SELECT * FROM reservations
WHERE room_id = 101
  AND during && daterange('2024-03-15', '2024-03-20');

Choosing the Right Index

Use Case Index Type Why
Most queries B-tree Versatile, handles ranges and equality
UUID lookups only Hash Slightly faster equality, smaller size
Tag filtering GIN Designed for array containment
Full-text search GIN Indexes individual words efficiently
JSONB queries GIN Indexes all keys and values
"Near me" searches GiST Spatial indexing for proximity
Date range overlaps GiST Range containment/overlap operators

Key Takeaways

  • B-tree is your default - Works for 90% of use cases
  • Hash for pure equality - When you'll ONLY ever do = comparisons
  • GIN for composite values - Arrays, JSONB, full-text search
  • GiST for spatial/ranges - Geographic queries, overlapping ranges

Next up: Composite Indexes - How multi-column indexes work and why column order matters.

🧠 Quick Quiz

Test your understanding of this lesson.

1

Which index type is best for exact equality lookups only (no ranges)?

2

You need to search for documents containing ANY of several tags. Which index type?

3

Which index type would you use for 'find all stores within 5km of a point'?

Why Indexes Matter