SQL Best Practices
SQL is forgiving enough to let you write something that works today and quietly causes a production incident, a slow-motion performance problem, or a security hole later. The practices below aren't exotic — they're the habits experienced engineers apply by default, precisely because the failure modes they prevent are so common and so costly.
Before you run anything destructive
Always attach a
WHEREclause toUPDATEandDELETE— anUPDATEorDELETEwith noWHEREclause touches every single row in the table. This is one of the most common and most damaging mistakes in SQL.Verify with a
SELECTfirst — before running theUPDATE/DELETE, run the equivalentSELECTwith the sameWHEREclause and confirm it returns exactly the rows you intend to change.Wrap risky changes in a transaction — start with
BEGIN, run the statement, inspect the result, and onlyCOMMITonce you are sure it is correct;ROLLBACKcosts nothing if something looks wrong.
Check before you commit
BEGIN; SELECT * FROM subscriptions WHERE status = 'expired' AND renewed_at IS NULL; -- confirm this is exactly the set of rows you expect DELETE FROM subscriptions WHERE status = 'expired' AND renewed_at IS NULL; -- look at the row count returned by the DELETE — does it match the SELECT? COMMIT;
Writing queries and code around them
Use parameterized queries, never string concatenation — building SQL by splicing raw input into a string is both a SQL injection risk and a correctness bug waiting to happen.
Avoid
SELECT *in production code — list the columns you actually need. It documents intent, avoids pulling unnecessary data over the network, and stops your code from silently breaking when someone adds a column.Use meaningful, consistent naming — plural table names, singular foreign key names like
user_id, and a consistent casing convention make a schema self-documenting instead of a guessing game.Normalize deliberately, and denormalize deliberately too — start from a normalized design to avoid update anomalies and redundant data, but treat denormalization as a conscious, documented performance trade-off, not an accident.
Performance and operational hygiene
Index columns used in
WHERE,JOIN, andORDER BY— an unindexed column used to filter or join a large table forces a full table scan every time the query runs.Use transactions for multi-step operations — any sequence of statements that must all succeed or all fail together belongs inside
BEGIN/COMMIT, not left to run as independent statements.Use
EXPLAINto verify performance — don’t guess whether a query is using an index or scanning the whole table; ask the database’s query planner directly before assuming a query is fast enough.Apply the principle of least privilege — every database account, human or application, should hold only the privileges it actually needs to do its job.