{"post":{"slug":"database-indexing-explained-2026","title":"Database indexing explained in 2026: what every developer must know","excerpt":"Bad indexes cause more production incidents than almost any other root cause. Here is the 2026 guide to choosing, building, and maintaining the right ones.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">I</span>ndexes are the single highest-leverage performance tool in a relational database, and they're also the most commonly misused. Add too few and queries crawl. Add too many and writes stall, storage balloons, and the query planner makes bad choices. The 2026 guide to getting this right follows.\n\n## What changed in 2026\n\n- **Postgres 16's parallel index builds** cut the time to add a large index on live tables significantly — `CREATE INDEX CONCURRENTLY` is now even less disruptive.\n- **Vector indexes (HNSW via pgvector 0.7+)** became a mainstream concern as teams added semantic search to relational workloads. The principles of index selectivity still apply.\n- **DuckDB's ART (Adaptive Radix Tree) indexes** are now the reference for analytical query engines, distinct from OLTP B-tree assumptions.\n- **MySQL 8.4 improved invisible indexes** — you can mark an index invisible and watch query plans change before actually dropping it.\n\n## How a B-tree index works\n\nA B-tree index is a balanced tree where every leaf node holds a sorted list of column values plus pointers to heap rows. A full table scan touches every page (O(n) I/O); an index scan walks the tree height (typically 3–4 levels for millions of rows), finds the matching leaf, and follows pointers — O(log n).\n\nThe catch: those heap pointer follows are random I/O. For queries returning >5–15% of rows, the planner often chooses a sequential scan anyway. That's correct behaviour.\n\n## Index types and when to use each\n\n| Type | Best for | Notes |\n| --- | --- | --- |\n| B-tree | Equality, range, ORDER BY | Default; use unless you have a reason not to |\n| GIN | Arrays, full-text search, JSONB keys | Slower writes, big win for containment queries |\n| GiST | Geometric, range types, nearest-neighbour | Lossy or exact depending on operator class |\n| BRIN | Huge append-only tables (logs, events) | Tiny index, only useful when data is physically ordered |\n| Hash | Pure equality (=), no range | Rarely worth it over B-tree in Postgres |\n| HNSW (pgvector) | Vector similarity search | Tune `m` and `ef_construction` for recall vs speed |\n\n## Composite indexes: column order rules\n\n```sql\n-- Query: WHERE status = 'active' AND created_at > '2026-01-01'\n-- Correct order: equality column first, range column second\nCREATE INDEX idx_orders_status_created\n  ON orders (status, created_at);\n```\n\nThe rule: **equality predicates first, then range predicates**. The index can scan a contiguous range only for columns up to and including the first range predicate. Columns after a range predicate in the WHERE clause won't benefit from the composite index.\n\n```sql\n-- Wrong order for the query above:\nCREATE INDEX idx_wrong ON orders (created_at, status);\n-- This forces a range scan on created_at and can't use status from the index directly.\n```\n\n## Covering indexes\n\nA covering (or \"index-only\") index includes all columns the query needs, eliminating the heap fetch entirely.\n\n```sql\n-- Query: SELECT email FROM users WHERE tenant_id = 42 ORDER BY created_at\nCREATE INDEX idx_users_tenant_email ON users (tenant_id, created_at) INCLUDE (email);\n```\n\nThe `INCLUDE` clause (Postgres 11+, SQL Server) adds columns to the leaf level without being part of the sort key. This avoids a heap fetch but keeps the index smaller than putting email in the key.\n\n## How to find missing indexes\n\n```sql\n-- Postgres: queries doing sequential scans on large tables\nSELECT schemaname, tablename, seq_scan, seq_tup_read,\n       idx_scan, idx_tup_fetch\nFROM pg_stat_user_tables\nWHERE seq_scan > 100\nORDER BY seq_tup_read DESC;\n```\n\nThen run `EXPLAIN (ANALYZE, BUFFERS)` on the slow queries. Look for `Seq Scan` on tables with many rows; look for `Buffers: shared hit=...read=` — reads are I/O, hits are cache.\n\n## How to pick\n\n1. **Start from slow queries, not schema.** Profile first (`pg_stat_statements`, slow query log).\n2. **Index columns in WHERE and JOIN ON.** Foreign key columns are almost always worth indexing.\n3. **Use composite indexes to cover common query patterns** — don't create three single-column indexes when one composite handles all three predicates.\n4. **Add INCLUDE columns for high-frequency read paths** to go index-only.\n5. **Set up `pg_stat_user_indexes` monitoring** to find unused indexes and drop them.\n\n## Common mistakes\n\n**Indexing low-cardinality columns alone.** A B-tree on a boolean `is_deleted` column with 99% false rows is nearly useless — the planner skips it. Combine with a high-cardinality column or use a partial index.\n\n```sql\n-- Partial index: only index active rows\nCREATE INDEX idx_orders_active ON orders (customer_id) WHERE status = 'active';\n```\n\n**Not accounting for write amplification.** Every INSERT, UPDATE on an indexed column, and DELETE must update all relevant indexes. A table with 12 indexes has 12× the write overhead.\n\n**Index bloat.** Dead tuples from UPDATEs leave bloat in indexes. Run `REINDEX CONCURRENTLY` or use `pg_repack` periodically on high-churn tables.\n\n**Indexing JSONB columns entirely.** A GIN index on a full JSONB column is large and slow to update. Index specific paths using a computed/generated column instead.\n\n## What to skip\n\n- **Indexes on every column \"just in case\"** — unused indexes consume storage and slow every write.\n- **Hash indexes for range queries** — they can't do it.\n- **Manual index hints** in Postgres — the planner is good; if it's making bad choices, fix statistics or table bloat first.\n\n## FAQ\n\n**How do I know if my index is being used?**\n`SELECT * FROM pg_stat_user_indexes WHERE relname = 'your_table'` — check `idx_scan`. Zero or very low after significant traffic means the index is unused.\n\n**Does adding an index lock the table?**\n`CREATE INDEX CONCURRENTLY` (Postgres) does not take a full table lock and is safe in production, though it takes longer and can fail if a concurrent transaction aborts — check for invalids with `\\d tablename` afterward.\n\n**Should I index foreign keys?**\nYes, almost always. Without an index on the FK column, a DELETE on the parent table triggers a sequential scan of the child table to enforce the constraint.\n\n**What is index selectivity?**\nSelectivity is the fraction of rows an index predicate eliminates. High selectivity (few matching rows, like a UUID) makes an index very useful. Low selectivity (like a gender column) makes it nearly worthless for reads.\n\n## Where to go next\n\nSee [SQL window functions in 2026](/blog/sql-window-functions-2026) for writing queries that get the most from your indexes, and [Caching strategies in 2026](/blog/caching-strategies-2026) for when to move hot query results out of the database entirely.","hue":318,"metaTitle":"Database Indexing Explained 2026: Types, Tips, Tradeoffs","metaDescription":"Learn database indexing in 2026. B-tree, GIN, BRIN, composite indexes, covering indexes, and the exact rules for choosing and maintaining them in production.","keywords":["database indexing explained 2026","b-tree index database","composite index column order","covering index postgres","gin index full text search","brin index time series","sql index performance 2026","how indexes work database","index bloat postgres","database query optimization 2026"],"tldr":["**An index is a sorted copy of one or more columns** — the database walks it instead of scanning every row, dropping query time from O(n) to O(log n).","**B-tree indexes handle equality and range queries; cover 90% of needs** — GIN for full-text, BRIN for append-only time-series, hash for pure equality.","**Composite index column order matters enormously** — put the highest-cardinality, most-filtered column first unless a range scan forces otherwise.","**Every index slows writes** — index only columns that appear in WHERE, JOIN ON, or ORDER BY of actual slow queries.","**Skip:** adding indexes speculatively; always profile with EXPLAIN ANALYZE before and after."],"readTime":"5 min read","wordCount":1063},"related":[{"slug":"indexes-explained-2026","title":"Database indexes explained in 2026: what every developer must know","excerpt":"Indexes make or break query performance. Here is the 2026 guide covering B-tree, GIN, GiST, and partial indexes with concrete examples and tradeoffs.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":309},{"slug":"how-to-optimize-sql-queries-2026","title":"How to optimize SQL queries in 2026: indexes, plans, and real fixes","excerpt":"Slow SQL is rarely a mystery — the query plan tells you exactly what is wrong. This 2026 guide covers the tools and techniques to find and fix slow queries fast.","category":"code","date":"April 1, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":309},{"slug":"gin-indexes-explained-2026","title":"GIN Indexes: Indexing Values Inside a Column","excerpt":"A B-tree indexes a column's value. A GIN index indexes the things inside it — array elements, document keys, words in a text field — which is a different problem.","category":"code","date":"January 7, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":316}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"cookies-vs-tokens-explained-2026","title":"Cookies vs tokens explained in 2026: which to use for auth sessions","excerpt":"Cookies and tokens are both valid ways to maintain authentication state, but they have different security tradeoffs. Here is the honest 2026 comparison for web and mobile apps.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":316},"older":{"slug":"debit-vs-credit-2026","title":"Debit vs credit card in 2026: which should you actually use for daily spending?","excerpt":"Debit feels safer and credit feels dangerous — but the data and protections in 2026 make credit the better daily-use tool for most financially responsible people.","category":"finance","date":"March 10, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":320}}