An SQL style guide for readable queries
SQL is read far more often than it is written. Queries live for years in migrations, reports, views and application code, and someone has to understand each of them when it becomes slow or returns the wrong rows. A consistent style makes that much easier, and some formatting choices actually prevent bugs. This guide proposes conventions that most teams can adopt as they are, with the reasoning behind each.
One clause per line
Start each major clause on a new line at the same indentation: SELECT, FROM, JOIN, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. Put each selected column and each condition on its own line, indented beneath its clause.
SELECT
o.id,
o.created_at,
c.email,
SUM(i.quantity * i.unit_price) AS total
FROM orders AS o
JOIN customers AS c ON c.id = o.customer_id
JOIN order_items AS i ON i.order_id = o.id
WHERE o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
GROUP BY o.id, o.created_at, c.email
ORDER BY total DESC
LIMIT 50;
The shape of the query is visible at a glance, and a change to one column or condition shows up as a one-line diff in code review. The SQL Formatter produces this layout automatically for most dialects.
Keyword case
Write keywords in uppercase and identifiers in lowercase. SQL does not care, but readers do: uppercase keywords separate the structure of the query from the names of your tables and columns. Some teams prefer all lowercase, which is also fine; the important thing is to pick one and let a formatter enforce it, so nobody spends review time on it.
Naming
- Use
snake_casefor tables and columns. Most databases fold unquoted identifiers to one case, soCustomerIdbecomescustomeridin PostgreSQL unless it is quoted everywhere, forever. - Choose plural or singular table names and stick with it.
ordersandcustomerin the same schema invite typos. - Name foreign keys after the table they point to:
customer_idreferencescustomers.id. - Name timestamps by meaning and type, such as
created_atandpaid_at, and booleans as statements, such asis_active. - Avoid reserved words like
user,orderandgroupas names; they require quoting in some databases.
Aliases
Use short, meaningful table aliases, typically the first letter or two of the table name, and always write AS for column aliases. Qualify every column with its table alias once more than one table is involved. That avoids ambiguity errors when a column with the same name is later added to another table, and it tells readers exactly where each value comes from. Avoid meaningless aliases like a, b, c or t1, which force readers to scroll back to the FROM clause.
Joins
Write joins explicitly with JOIN ... ON, never as comma-separated tables with conditions in WHERE. With the old comma style, forgetting one condition silently creates a cross join that multiplies rows. With explicit joins, a missing ON is a syntax error in most databases.
Write JOIN rather than INNER JOIN, and always spell out LEFT JOIN. Put the condition on the same line as the join when it fits, with the column of the newly joined table first, so readers see how each table connects to what came before.
Filters on outer joins
A frequent bug: filtering the right side of a LEFT JOIN in WHERE turns it back into an inner join, because rows with no match have NULL there and fail the condition. Put such filters in the ON clause instead:
LEFT JOIN payments AS p
ON p.order_id = o.id
AND p.status = 'succeeded'
Conditions and parentheses
Start continuation lines with AND or OR, so conditions can be commented out or reordered without editing the previous line. Whenever a condition mixes AND and OR, add parentheses even if you know the precedence rules. WHERE a = 1 OR b = 2 AND c = 3 means a = 1 OR (b = 2 AND c = 3), which is often not what the author intended.
Remember that comparisons with NULL are never true: use IS NULL, and be careful with NOT IN against a subquery that can return NULL, which makes the whole condition return no rows. NOT EXISTS avoids that trap.
Use CTEs to name steps
Long queries with nested subqueries are hard to read from the inside out. Common table expressions let you read top to bottom, with each step named:
WITH paid_orders AS (
SELECT id, customer_id, total
FROM orders
WHERE status = 'paid'
),
customer_totals AS (
SELECT customer_id, SUM(total) AS lifetime_value
FROM paid_orders
GROUP BY customer_id
)
SELECT c.email, ct.lifetime_value
FROM customer_totals AS ct
JOIN customers AS c ON c.id = ct.customer_id
ORDER BY ct.lifetime_value DESC;
In modern PostgreSQL, MySQL 8 and SQL Server, CTEs are usually optimised just like subqueries, so readability costs nothing. Check the execution plan if you are on an older version.
Avoid SELECT * in code
SELECT * is convenient in an interactive session, but in application code and views it returns columns you do not need, breaks when columns are added or reordered, and prevents index-only scans. List the columns you use.
Comments
Comment the why, not the what. A note such as -- refunds are stored as negative totals, exclude them from revenue saves the next reader an hour. Use -- line comments; block comments are harder to nest and to spot in diffs.
Safety rules that are not about style
- Parameterise everything. Pass values as bind parameters (
$1,?,:name), never by concatenating strings. This prevents SQL injection and lets the database reuse plans. - Write
WHEREfirst when updating or deleting. When typing anUPDATEorDELETEby hand, write the condition before the rest, or run it as aSELECTfirst, inside a transaction. - Check plans. Run
EXPLAIN(orEXPLAIN ANALYZEon a copy of production data) for any new query on a large table.
Enforce it with tools
A style guide only works if it is applied automatically. Format SQL files with a formatter in your editor and in CI, using the dialect your database speaks, and lint them with a tool such as SQLFluff if you want rules beyond layout. When you need to read a query captured from an ORM log, format it first; you can also compare an old and new version of a migration in the Text Diff after formatting both the same way.