SQL Formatting Best Practices: Write SQL That Others Can Read
Unformatted SQL is a maintenance nightmare. This guide covers indentation conventions, JOIN alignment, CTE style, and how consistent formatting prevents logic bugs in complex queries.
Why SQL Formatting Matters
SQL has no enforced style. A query can be one line or one hundred, with keywords in any case, columns anywhere. This freedom creates two problems in real teams:
- Logic bugs hidden by poor formatting — a misplaced JOIN condition looks identical to a WHERE filter when everything is on one line.
- Review bottleneck — reviewers spend cognitive energy parsing layout instead of evaluating logic.
Consistent formatting is cheap to adopt and permanently solves both problems. Here is the style that works across dialects and teams.
Before and After
❌ Unformatted
select u.id,u.name,o.total,p.name as product from users u join orders o on u.id=o.user_id join order_items oi on o.id=oi.order_id join products p on oi.product_id=p.id where u.created_at > '2025-01-01' and o.status='completed' order by o.total desc limit 20
✅ Formatted
SELECT
u.id,
u.name,
o.total,
p.name AS product
FROM users AS u
JOIN orders AS o ON u.id = o.user_id
JOIN order_items AS oi ON o.id = oi.order_id
JOIN products AS p ON oi.product_id = p.id
WHERE
u.created_at > '2025-01-01'
AND o.status = 'completed'
ORDER BY o.total DESC
LIMIT 20;8 Formatting Rules That Work
- Uppercase SQL keywords.
SELECT,FROM,WHERE,JOIN— always uppercase. Column names and aliases — lowercase. This visual contrast makes structure instantly clear. - One clause per line. Each top-level keyword (
SELECT,FROM,WHERE,GROUP BY,ORDER BY) starts on its own line. - Indent column lists. Columns under
SELECTare indented 4 spaces. Put the comma at the start of each line (leading comma) — makes it trivial to comment out any single column. - Align JOIN conditions. The
ONkeyword aligns vertically with otherONkeywords. This makes it obvious at a glance which table each JOIN connects to. - AND / OR at the start of a line. In
WHEREandHAVING, start each condition withAND/ORat the beginning of a new line, indented. - Always use table aliases. Even for single-table queries, an alias (
uforusers) makes column ownership clear and simplifies future JOINs. - Name subqueries and CTEs meaningfully.
WITH recent_orders AS (...)beatsWITH t1 AS (...)every time. - End every statement with a semicolon. Required in some contexts, harmless in all — and makes multi-statement files unambiguous.
CTE Formatting Pattern
Common Table Expressions are powerful but easily become unreadable. Follow this layout:
WITH
active_users AS (
SELECT id, name, email
FROM users
WHERE is_active = TRUE
),
recent_orders AS (
SELECT user_id, COUNT(*) AS order_count, SUM(total) AS revenue
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY user_id
)
SELECT
u.name,
u.email,
COALESCE(ro.order_count, 0) AS orders_last_30d,
COALESCE(ro.revenue, 0) AS revenue_last_30d
FROM active_users AS u
LEFT JOIN recent_orders AS ro ON u.id = ro.user_id
ORDER BY ro.revenue DESC NULLS LAST;Mistakes That Cause Bugs
SELECT * FROM orders JOIN users ON orders.id = users.id
Joining on the wrong column — orders.id should be orders.user_id. Easy to miss on one line.
WHERE status = 'active' AND created_at > '2025-01-01' OR is_vip = TRUE
Operator precedence: AND binds tighter than OR. This is (status AND date) OR vip — probably not the intent. Use parentheses.
SELECT name, COUNT(*) FROM users GROUP BY id
Selecting a non-aggregated, non-grouped column. Silently returns arbitrary values in MySQL.
Format any SQL query instantly
Paste messy SQL to reformat it with consistent indentation and keyword casing — supports MySQL, PostgreSQL, SQLite, and more.
Open SQL Formatter →