SQL Anti-Patterns
Recognize the mistakes that quietly cost you
SELECT *, cursor pagination, N+1 reads, non-sargable predicates and COUNT over huge tables are the recurring mistakes that make warehouse queries slow or wrong.
What you will learn
- Identify the common SQL anti-patterns by signature
- Rewrite them into sargable, explicit, indexed variants
- Know when a 'trick' is a real fix versus a smell
What is this?
Certain SQL shapes recur in every codebase and are wrong in specific ways: SELECT * couples consumers, explicit row pagination thrashes, non-sargable predicates (func(column)) can't use indexes, and COUNT(*) over a giant table scans everything. Recognizing them by signature lets you fix root cause instead of patching symptoms.
Why do you need it?
- These patterns are the difference between a query that takes 4ms and one that takes 4 minutes on the same table.
- Each is cheap to fix once recognized and expensive when left to multiply.
- Interview loops test exactly these traps.
Mental model
Crossing a street against the light
A SQL anti-pattern
It usually works, feels convenient, and everyone does it — until the day it doesn't. The SQL traps are the same: tolerable at small scale, catastrophic at production volume.
How it works
- SELECT * pulls columns the query doesn't need — larger IO, wider rows, brittle to renames.
- OFFSET/LIMIT walks dropped tuples on every page: OFFSET 100000 means skipping 100k rows.
- func(column) in WHERE (LOWER(email) = ...) can't use a plain index unless it's an expression index.
- N+1: one query per related row instead of one join/batch — the API layer's classic.
- COUNT(*) on a big table scans all rows when an exact count isn't needed; approximate suffices.
Anti-pattern → safe rewrite.
In production
- A product-catalog API switched OFFSET to keyset pagination and p99 dropped from 2s to 40ms at 100k products.
- A report query replaced SELECT * with two explicit columns, halving IO by reading less from wide tables.
- An analytics dashboard replaced exact COUNT with pg_class.reltuples/statistics to render instantly.
Best practices
- Always enumerate the columns a query actually needs.
- Paginate with keyset idioms (WHERE key > :last ORDER BY key) on OLTP-style data.
- Keep predicates sargable: match the index; use expression indexes when you must wrap columns.
- Add uniqueness tests so N+1-inducing duplicate keys are caught at the source.
Common mistakes
- Writing OFFSET/LIMIT for deep admin exports that never make it past page 100.
- Putting LOWER/UPPER/EXTRACT around indexed columns and wondering why scans remain.
- SELECT * into a 40-column dump that pandas reads every night.
- Fixing N+1 by adding an index per loop instead of removing the loop.
Common issues
Deep pagination slows linearlyOFFSET re-scans the skipped rows every page
Switch to keyset pagination using an indexed unique column.
LOWER(email) filter ignores a perfect indexNon-sargable left-hand function
Create an expression index on LOWER(email) or store a precomputed normalized column.
Debugging tips
- Search the codebase for SELECT *, OFFSET, and per-row fetch loops as a triage pass.
- When a 'small change' slows production, diff the queries for a non-sargable predicate.
- Measure with EXPLAIN ANALYZE before declaring a pattern 'fixed'.
Interview checkpoint
Quick quiz
Which pagination approach stays fast at page 100,000?
Wrapping a column in LOWER() in the WHERE clause lets most planners use a plain column index.