Back to Blog
Performance11 min read2026-07-10

Database Indexing for Application Performance

A missing index can turn a 5ms query into a 5-second table scan. This guide explains how indexes work, when to add them, composite index ordering, and how to read query plans.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# 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:

  • WHERE clauses (the filters)
  • JOIN conditions (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 tableWrite-heavy table
Many indexesGreatWrite amplification, slow
Few indexesSlow readsFast 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 Scan on a large table = missing index (red flag).
  • Index Scan / Index Only Scan = the index is being used (good).
  • High rows estimates vs actual = stale statistics; run ANALYZE.
  • 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_active rarely 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 on email. 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

  1. 1Find slow queries (slow query log, pg_stat_statements).
  2. 2Run EXPLAIN ANALYZE on each.
  3. 3Add the smallest index that eliminates the scan.
  4. 4Re-run and confirm the plan changed.
  5. 5Watch write performance and storage.
  6. 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).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Performance

Browse all Performance articles →

See also