SQL Formatting Conventions That Make Queries Easier to Review
Clause-per-line layout, keyword casing, join alignment and CTEs. A practical style guide for SQL that your teammates can read at 2 a.m. during an incident.
Most SQL is read far more often than it is written. It is read in code review, in migration files, in dashboards, and at 2 a.m. when a slow query is taking a database down and someone has to understand it fast. Formatting does not change what a query does. It changes how quickly a person can see what it does.
This guide sets out practical SQL formatting conventions and the reasoning behind them. The SQL Beautifier breaks a one-line query into readable clauses as a starting point.
Why SQL gets unreadable
SQL was designed at IBM in the early 1970s by Donald Chamberlin and Raymond Boyce, first as SEQUEL, with the explicit goal of reading like English. It became an ANSI standard in 1986. A short query still reads that way:
SELECT name FROM users WHERE active = true;
Real queries do not stay short. ORMs generate queries with aliases like t0 and t1, dozens of columns and nested subqueries, all on one line. Monitoring tools and slow-query logs show them the same way. That is where a formatter earns its keep.
The core conventions
There is no single official style, but most guides, including Simon Holywell's widely referenced SQL Style Guide, agree on a handful of rules.
One major clause per line
SELECT o.id, o.total, c.name
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
WHERE o.created_at >= '2026-01-01'
AND o.status = 'shipped'
ORDER BY o.created_at DESC
LIMIT 50;
Starting SELECT, FROM, JOIN, WHERE, GROUP BY, HAVING, ORDER BY and LIMIT on new lines lets the eye scan the left edge and see the structure of the query immediately. This is the main thing the RedBison beautifier does.
Consistent keyword case
Upper-case keywords (SELECT, WHERE) with lower-case identifiers (orders, customer_id) is the most common convention. The database does not care; humans scanning a query can separate the language from the names at a glance. Lower-case keywords are also fine. Pick one and be consistent.
Each condition on its own line
WHERE o.status = 'shipped'
AND o.total > 100
AND (c.country = 'GB' OR c.country = 'IE')
Putting AND and OR at the start of lines makes conditions easy to comment out while debugging and makes diffs show exactly which condition changed. Always use parentheses when mixing AND and OR; AND binds tighter, and relying on that rule is a classic source of bugs.
Join conditions next to their joins
Write every join explicitly, with its condition on the same line or directly beneath:
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
LEFT JOIN refunds AS r ON r.order_id = o.id
The old comma-join style, FROM orders, customers WHERE orders.customer_id = customers.id, mixes join logic with filters and makes it easy to forget a condition and produce an accidental cross join.
Meaningful aliases
o for orders and c for customers is fine in a short query. In a long one, ord and cust, or the full table name, saves the reader from keeping a mental lookup table. Avoid a, b, c assigned in arbitrary order.
Name your columns
Avoid SELECT * in application code and views. It hides which columns are used, breaks when columns are added or reordered, and transfers data nobody needs. Listing columns one per line also gives clean diffs when you add one.
Comment the why, not the what
A one-line comment above a non-obvious filter, such as -- exclude test accounts created by QA, is worth more than any amount of alignment. SQL rarely explains its own business rules, and the next person to touch the query will not know why status <> 'archived' is there unless you tell them.
Common table expressions
For anything with more than one level of subquery, CTEs (WITH clauses) usually read better:
WITH recent_orders AS (
SELECT customer_id, SUM(total) AS spend
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY customer_id
)
SELECT c.name, r.spend
FROM recent_orders AS r
JOIN customers AS c ON c.id = r.customer_id
ORDER BY r.spend DESC;
Each step gets a name and can be read top to bottom. Be aware that some databases historically treated CTEs as optimization fences; PostgreSQL did until version 12. Modern versions inline them where possible.
Formatting during an incident
When you are handed a slow query from a log, my routine is:
- Format it so the clauses and joins are visible.
- Count the joins and check each has a condition. A missing condition is a cross join.
- Look at the
WHEREclause for functions wrapped around indexed columns, likeWHERE DATE(created_at) = ..., which can prevent index use. - Look for
ORconditions and leading-wildcardLIKE '%...'patterns that defeat indexes. - Run
EXPLAIN(orEXPLAIN ANALYZEon a replica) to see the actual plan.
Formatting is step one because you cannot reason about a query you cannot read.
About automatic formatters
The RedBison beautifier is keyword-based: it inserts line breaks before major clauses and joins. That makes it fast and dialect-agnostic, but it does not parse SQL, so check the output where SQL keywords appear inside string literals or comments.
For a whole codebase, a parser-based formatter wired into your editor and CI gives consistent results. Options include SQLFluff (which also lints), pgFormatter for PostgreSQL, and the sql-formatter package for JavaScript projects. Agreeing a style once and automating it ends formatting debates in review.
Related tools
- SQL Query Builder to assemble queries visually
- SQL Explain Visualizer for reading execution plans
- Online Diff Tool to compare two versions of a query