SQL Safety Rules
Writes
- Never write an UPDATE or DELETE without a WHERE clause. If you truly mean every row, say so explicitly in a comment on the line above and get it approved.
- Count before you write. Run the SELECT with the same WHERE first and check the row count against what you expect. A number two orders of magnitude off means the predicate is wrong.
- Wrap multi-statement writes in a transaction, and know whether your DDL is transactional. In Postgres it mostly is. In MySQL it mostly is not.
- Bound every bulk write. Delete or update in batches with a LIMIT and a loop, not in one statement that locks a million rows.
- Never write to production from an interactive session when a reviewed migration would do.
Reads
- Every query on a large table needs an index-supported predicate. Check the plan. A sequential scan in a hot path is a future incident.
- No SELECT * in application code. It breaks silently when a column is added and ships columns you do not need over the wire.
- Paginate by a stable key, not by OFFSET. OFFSET on a large table gets slower the deeper it goes, and skips rows when data shifts underneath it.
Migrations
- Every migration needs a down, or an explicit note that it is irreversible and why that is acceptable.
- Adding a NOT NULL column with a default rewrites the table on older engines. Add nullable, backfill in batches, then add the constraint.
- Never rename or drop a column in the same deploy that stops using it. Ship the code that ignores it, deploy, then drop in a later migration. Otherwise the old running version breaks the instant the migration lands.
- Create indexes concurrently where the engine supports it. A plain CREATE INDEX takes a write lock for the duration.
- Backfill outside the migration, in batches, with a resumable cursor. A backfill inside a migration holds a lock for as long as it runs.
Injection and identity
- Parameterize every value. Always. String interpolation into SQL is the bug, even when the input "cannot" contain a quote.
- Identifiers cannot be parameterized, so validate table and column names against an allowlist rather than passing them through.
Before running anything against production
- Know the row count the statement will touch.
- Know the lock it takes and for how long.
- Know the rollback. If it is a restore from backup, know how long the restore takes and say so out loud before running.