Writing Readable SQL: A Guide to Formatting Queries for Teams
SQL is one of the oldest languages still in daily use, and one of the most inconsistently written. Walk into any engineering team and you'll find queries that look completely different from one another โ some uppercase keywords, some lowercase, some with every clause on its own line, others smashed onto a single line that scrolls for three screen-widths. This inconsistency isn't just cosmetic. It slows down code review, makes debugging harder, and hides logic errors that a little whitespace would make obvious.
This guide covers the formatting conventions that experienced teams converge on, explains the reasoning behind each one, and gives you practical patterns you can adopt immediately.
Format SQL instantly: Paste any query into our free SQL Formatter and get clean, consistently formatted output.
Open SQL Formatter โTable of Contents
Why SQL Formatting Matters More Than You Think
Unlike most programming languages, SQL has no compiler to catch style problems. A poorly structured query that returns correct results can hide logic errors for months until a data change or edge case exposes them. Consider this query:
select u.id,u.name,count(o.id) from users u left join orders o on u.id=o.user_id where u.status='active' and u.created_at>'2026-01-01' group by u.id,u.name having count(o.id)>0 order by count(o.id) desc limit 100
It works. But now consider what happens when a colleague needs to add a condition, or a bug report comes in saying the count is wrong. The logic is buried. Now the same query, formatted:
SELECT
u.id,
u.name,
COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
WHERE
u.status = 'active'
AND u.created_at > '2026-01-01'
GROUP BY
u.id,
u.name
HAVING COUNT(o.id) > 0
ORDER BY order_count DESC
LIMIT 100
Every clause is instantly scannable. The join condition is clearly subordinate to the JOIN clause. Adding a new WHERE condition is a one-line change. The alias makes the COUNT readable downstream.
Keyword Casing: Pick One and Stick to It
The SQL standard doesn't care whether you write SELECT or select โ both are valid. But your team should pick one convention and enforce it everywhere.
Uppercase keywords are the most common convention in professional environments. The visual contrast between SELECT and the identifiers user_id, created_at makes it immediately clear what is language syntax and what is your schema. Most SQL style guides recommend uppercase keywords.
Lowercase keywords are popular in teams that use ORMs and code-generation tools where SQL is embedded in string literals. The important thing is not which you choose but that you choose consistently. Never mix casing within a project.
Clause Placement: Every Clause Gets Its Own Line
The most impactful structural rule: each major clause starts on its own line. Major clauses are SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, JOIN variants, and UNION.
This makes it trivially easy to find where the WHERE clause starts, comment out a single clause during debugging, add a new JOIN without restructuring surrounding lines, and see at a glance how many tables are involved.
SELECT column lists
Put each column on its own line, indented under SELECT. Adding, removing, or reordering columns produces cleaner diffs in version control and avoids the classic "missing comma" bug hidden in a long single line.
JOIN conditions
Indent the ON condition under its JOIN clause. When you have four or five joins, this makes it visually clear which condition belongs to which join.
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
LEFT JOIN products p
ON o.product_id = p.id
WHERE conditions
Put each condition on its own line with AND or OR at the start of the line. Putting the boolean operator at the start of the line makes it much easier to comment out a single condition during debugging โ you don't have to edit the previous line.
WHERE
u.status = 'active'
AND u.created_at > '2026-01-01'
AND u.country_code = 'US'
Aliases: When and How to Use Them
Always use the AS keyword when aliasing โ both COUNT(id) AS order_count and COUNT(id) order_count are valid SQL, but explicit AS makes intent clear.
Alias calculated columns with meaningful names. COUNT(o.id) AS order_count propagates to result column headers and makes downstream code cleaner.
Qualify all column references with the table alias whenever joining more than one table. SELECT id becomes ambiguous the moment you add a second table. SELECT u.id is always unambiguous.
Indentation and Subqueries
Subqueries in a FROM clause should be indented, with the closing parenthesis aligned with the opening:
SELECT
outer.user_id,
outer.total_orders
FROM (
SELECT
user_id,
COUNT(*) AS total_orders
FROM orders
WHERE status = 'completed'
GROUP BY user_id
) AS outer
WHERE outer.total_orders > 5
CTEs (Common Table Expressions) are often more readable than subqueries. Format them with the same clause-per-line rules:
WITH active_users AS (
SELECT id, name, email
FROM users
WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
)
SELECT
u.name,
COALESCE(o.order_count, 0) AS orders
FROM active_users u
LEFT JOIN user_orders o
ON u.id = o.user_id
Common Anti-Patterns to Avoid
SELECT * is almost always wrong in production. It makes queries fragile to schema changes and pulls unnecessary data.
Implicit joins using comma syntax (FROM users u, orders o WHERE u.id = o.user_id) should be replaced with explicit JOIN syntax. Explicit joins make it clearer when a condition is accidentally missing, which in implicit syntax produces a cartesian product.
Magic numbers and strings without explanation. WHERE status = 3 is a maintenance nightmare. Use a comment or CTE that names the value.
Deeply nested subqueries โ more than two levels deep are very hard to debug. Refactor into CTEs.
Enforcing Consistency Across a Team
sqlfluff is the most comprehensive open-source SQL linter and formatter, with support for multiple dialects (PostgreSQL, MySQL, BigQuery, Snowflake) and configurable rules. It can format on save in most editors and run in CI pipelines. For dbt projects, dbt format handles SQL formatting. Most modern SQL IDEs have built-in formatters you can configure to match your team's style.
Document your team's choices in a SQL style guide and review formatting in code review. Once established and tooled, developers stop thinking about it โ and that's exactly when it's working.
