Appearance
Indexes
If
WHERE email = ?is your login lookup and it's slow, you probably forgot an index. Or the table is tiny and you're imagining things 🤔
What an index is (FE analogy)
Think of a book index: instead of scanning every page for "pagination", you jump to the page number.
A database index is a separate structure (often a B-tree) that maps column values → row locations. Speeds up lookups, filters, joins, and sorts - at the cost of extra storage and slower writes (index must update too).
Creating one
sql
CREATE INDEX idx_contacts_email ON contacts(email);
CREATE INDEX idx_contacts_company_id ON contacts(company_id);
-- composite: useful when you filter by both
CREATE INDEX idx_deals_stage_owner ON deals(stage, owner_id);Unique indexes also enforce uniqueness:
sql
CREATE UNIQUE INDEX idx_contacts_email_unique ON contacts(email);
-- (UNIQUE constraint on the column already creates one under the hood)When you want one
Good candidates:
- columns in
WHERE/JOIN … ONthat you hit constantly (email,company_id,owner_id) - foreign keys (joining parents/children)
- sort + filter combos for list pages (
stage,updated_at)
Skip / wait on:
- tiny tables (full scan is fine)
- columns that almost never appear in filters
- low-selectivity flags alone (
is_activeon a table that's 95% active) - sometimes useful in composites, rarely alone
List page mental model
sql
SELECT id, title, amount, stage
FROM deals
WHERE stage = 'negotiation'
AND owner_id = 3
ORDER BY updated_at DESC
LIMIT 20;An index like (stage, owner_id) or (owner_id, stage, updated_at) can turn "scan the whole deals table" into "jump to the matching slice". Exact best index depends on query patterns - measure when it matters.
EXPLAIN (peek under the hood)
sql
EXPLAIN QUERY PLAN
SELECT * FROM contacts WHERE email = 'ada@example.com';Look for USING INDEX … vs SCAN contacts. You don't need to memorize every line - just notice whether you're scanning the whole table.
Soft-delete gotcha
sql
WHERE deleted_at IS NULL AND company_id = 10Partial indexes (Postgres loves them; SQLite supports them too) can index only "live" rows:
sql
CREATE INDEX idx_contacts_company_live
ON contacts(company_id)
WHERE deleted_at IS NULL;Handy for CRMs where most queries ignore deleted rows.
Takeaway
Indexes are how backends keep your infinite scroll feeling infinite instead of "loading…". When someone says "we need an index on that FK", they're usually protecting a screen you built.
Basics done. Time to build something shaped like a product: CRM schema design.