# Database Indexing for Application Performance
The most common cause of a slow application is a slow query, and the most common cause of a slow query is a missing index. Indexes are the highest-leverage performance tool most developers underuse — a single well-placed index can take a query from a multi-second full table scan to sub-millisecond. Let's make indexing intuitive.
What an index actually is
An index is a separate, sorted data structure that lets the database find rows without scanning the whole table — like the index at the back of a book. Without it, finding all orders for user_id = 42 means reading every row (a *sequential scan*). With an index on user_id, the database jumps straight to the matching rows.
Most relational databases use a B-tree index by default: a balanced tree that supports equality (=), range (<, >, BETWEEN), and sorted access in logarithmic time. Other index types exist (hash, GIN, GiST, BRIN in Postgres) for specialized cases, but B-tree covers the vast majority of needs.
When an index helps
Add indexes for columns that appear in:
WHEREclauses (the filters)JOINconditions (foreign keys especially)ORDER BY/GROUP BY(sorting and grouping)
A classic miss: foreign key columns are *not* automatically indexed in many databases. If you join orders to users on orders.user_id constantly, that column needs an index even though it's a foreign key.
CREATE INDEX idx_orders_user_id ON orders (user_id);The cost of indexes
Indexes aren't free. Every index must be updated on INSERT, UPDATE, and DELETE, so they slow down writes and consume storage. The trade-off:
| Read-heavy table | Write-heavy table | |
|---|---|---|
| Many indexes | Great | Write amplification, slow |
| Few indexes | Slow reads | Fast writes |
The goal is the *minimum set of indexes* that covers your real query patterns — not an index on every column. Unused indexes are pure overhead; periodically audit and drop them.
Composite indexes and column order
A composite (multi-column) index covers queries that filter on multiple columns — but column order is everything. A B-tree index is sorted left-to-right, like a phone book sorted by last name then first name.
CREATE INDEX idx_orders_user_status ON orders (user_id, status);This index helps:
WHERE user_id = 42(uses leftmost column) ✅WHERE user_id = 42 AND status = 'paid'(uses both) ✅
But not:
WHERE status = 'paid'(skips the leftmost column) ❌
This is the leftmost-prefix rule. Order columns by selectivity and query patterns — generally put the columns used in equality filters first, then range/sort columns. A common guideline: equality columns, then sort columns, then range columns.
Covering indexes
If an index contains *all* the columns a query needs, the database can answer the query from the index alone without touching the table — an index-only scan. This is a covering index:
-- Query only needs user_id and total
SELECT total FROM orders WHERE user_id = 42;
-- This index covers it entirely
CREATE INDEX idx_orders_covering ON orders (user_id) INCLUDE (total);Covering indexes can dramatically speed up hot read paths at the cost of a larger index.
Read the query plan — don't guess
Never optimize blind. Use EXPLAIN ANALYZE (Postgres/MySQL) to see what the planner actually does:
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 42 AND status = 'paid';What to look for:
Seq Scanon a large table = missing index (red flag).Index Scan/Index Only Scan= the index is being used (good).- High
rowsestimates vs actual = stale statistics; runANALYZE. - Expensive sorts = consider an index that provides the sort order.
The planner is cost-based and depends on up-to-date statistics. If estimates are wildly off from reality, refresh stats before blaming the index.
Common indexing mistakes
- Indexing low-cardinality columns alone. An index on a boolean
is_activerarely helps — too few distinct values. Use a partial index instead:CREATE INDEX ... WHERE is_active = true. - Functions on indexed columns.
WHERE lower(email) = '...'won't use an index onemail. Use an expression index:CREATE INDEX ON users (lower(email)). - Leading wildcards.
LIKE '%term'can't use a standard B-tree. Use full-text search or trigram indexes. - Too many indexes. Every redundant index taxes writes. Drop unused ones.
- Ignoring write impact. Profile writes, not just reads.
Partial and specialized indexes
For large tables where you only query a subset, a partial index is smaller and faster:
-- Only index unprocessed jobs
CREATE INDEX idx_jobs_pending ON jobs (created_at) WHERE status = 'pending';For JSON, arrays, and full-text in Postgres, GIN indexes shine; for huge append-only time-series, BRIN indexes are tiny and effective. Match the index type to the access pattern.
A workflow that works
- 1Find slow queries (slow query log,
pg_stat_statements). - 2Run
EXPLAIN ANALYZEon each. - 3Add the smallest index that eliminates the scan.
- 4Re-run and confirm the plan changed.
- 5Watch write performance and storage.
- 6Periodically drop unused indexes.
Indexes on managed databases
Indexing is something you do in your schema regardless of where the database runs — but a managed database removes the operational distractions so you can focus on it. PandaStack's managed databases (PostgreSQL 14.x/16.x, MySQL 5.7/8.x, MongoDB, Redis via KubeBlocks) come with scheduled and manual backups and are auto-wired into your app via DATABASE_URL, so you can iterate on schema and indexes against a real managed instance without provisioning servers yourself.
References
- [Use The Index, Luke! (SQL indexing guide)](https://use-the-index-luke.com/)
- [PostgreSQL — Indexes documentation](https://www.postgresql.org/docs/current/indexes.html)
- [PostgreSQL — EXPLAIN](https://www.postgresql.org/docs/current/sql-explain.html)
- [MySQL — Optimization and Indexes](https://dev.mysql.com/doc/refman/8.0/en/optimization-indexes.html)
- [PostgreSQL — pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html)
Iterating on schema and indexes? PandaStack's free tier includes a managed database wired into your app automatically. [Start at dashboard.pandastack.io](https://dashboard.pandastack.io).