SQL Best Practices
SQL Best Practices
A reference guide for writing clean, efficient, and maintainable SQL.
Naming Conventions
- Use snake_case for table and column names (
customer_orders, notCustomerOrders) - Table names should be singular (
customer, notcustomers) - Primary keys should be named
idortable_name_id - Foreign keys should mirror the referenced column (
customer_id) - Avoid reserved words as column names (
date,name,type) - Be descriptive —
created_atis better thandt
Query Writing
SELECT
- Never use
SELECT *in production queries — always name your columns explicitly - Alias columns when the name is ambiguous or computed:
SUM(amount) AS total_amount - Use table aliases for readability, especially in joins:
c.customer_id
WHERE
- Always filter on indexed columns where possible
- Avoid wrapping columns in functions in WHERE clauses — it prevents index use:
-- Bad WHERE YEAR(created_at) = 2024 -- Good WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01' - Use
EXISTSinstead ofINfor subqueries against large tables
JOINs
- Always use explicit JOIN syntax (
INNER JOIN,LEFT JOIN) — never implicit comma joins - Join on indexed columns
- Be explicit about join type — don't use
JOINwhen you meanLEFT JOIN - Filter in the
ONclause for outer joins, not theWHEREclause
ORDER BY
- Only use
ORDER BYwhen order actually matters — it adds overhead - Never rely on implicit ordering
Indexing
- Index foreign key columns
- Index columns frequently used in WHERE, JOIN, and ORDER BY clauses
- Avoid over-indexing — indexes slow down writes
- Use composite indexes for multi-column filter patterns (column order matters)
- Regularly review unused indexes
Data Types
- Use the most specific type that fits the data (
TINYINTvsINTvsBIGINT) - Use
VARCHAR(n)with a realisticn— don't default everything toVARCHAR(255) - Store dates as
DATEorDATETIME, not as strings - Use
DECIMALfor currency — neverFLOATorDOUBLE - Use
BOOLEAN/BITfor true/false flags
Transactions
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
NULL Handling
- Understand that
NULLis not equal to anything, including itself - Use
IS NULL/IS NOT NULL, never= NULL - Use
COALESCE()to provide fallback values - Design columns to be
NOT NULLwhere possible and use defaults
Performance
- Use
EXPLAIN/EXPLAIN ANALYZEto understand query plans - Avoid
SELECT DISTINCTas a crutch — fix the root cause of duplicates - Use pagination (
LIMIT/OFFSETor keyset pagination) for large result sets - Avoid correlated subqueries — rewrite as JOINs where possible
- Use CTEs (
WITH) for readability, but be aware some engines materialize them
Security
- Always use parameterized queries / prepared statements — never string-concatenate user input
- Apply principle of least privilege — app accounts should not have DBA rights
- Avoid exposing internal IDs in APIs where possible
- Audit and log access to sensitive tables
- Never store passwords in plain text — store hashes only
Maintenance
- Use migrations for all schema changes — never modify production schema manually
- Document tables and columns with comments
- Keep a changelog of significant schema changes
- Regularly run
ANALYZE/UPDATE STATISTICSto keep query planner accurate - Archive or partition large tables rather than letting them grow unbounded
Style
- Capitalize SQL keywords:
SELECT,FROM,WHERE,JOIN - One clause per line for complex queries
- Indent subqueries and CTEs consistently
- Comment non-obvious logic
-- Calculate 30-day rolling revenue per customer
SELECT
c.id,
c.name,
SUM(o.amount) AS rolling_revenue
FROM customer c
INNER JOIN orders o
ON o.customer_id = c.id
WHERE o.created_at >= DATEADD(day, -30, GETDATE())
GROUP BY c.id, c.name
ORDER BY rolling_revenue DESC;
No comments to display
No comments to display