SQL Formatting Standards

SQL is read far more often than written, usually by someone else, usually under time pressure. Formatting is cheap insurance.

SQL has an unusual property: it is read vastly more often than it is written, and the reader is frequently not the author and frequently in a hurry. A query written once during a feature launch gets read during an incident eight months later by someone with no context. Unformatted SQL makes that reading actively error-prone.

There is also a mechanical benefit that pays off immediately in code review.

Why SQL formatting matters more than most code

Version control diffs are line-based. In a single-line query, editing one predicate rewrites the entire line, so the diff shows everything as changed and a reviewer cannot see what you actually did. In formatted SQL, one clause per line means one changed line.

- select u.id,u.email,count(o.id) as c from users u left join orders o on o.user_id=u.id where u.active=true and o.status='paid' group by u.id,u.email having count(o.id)>3 order by c desc limit 50;

+ select u.id,u.email,count(o.id) as c from users u left join orders o on o.user_id=u.id where u.active=true and o.status='shipped' group by u.id,u.email having count(o.id)>3 order by c desc limit 50;

Can you see the change? Neither can your reviewer. Now the same change, formatted:

    WHERE u.active = true
-     AND o.status = 'paid'
+     AND o.status = 'shipped'

Obvious. This is the single strongest argument for formatting SQL, and it applies to every query you will ever commit.

Before and after

-- Before
select u.id,u.email,count(o.id) as order_count,sum(o.total_cents)/100 as revenue from users u left join orders o on o.user_id=u.id and o.status<>'cancelled' where u.created_at>='2026-01-01' and u.deleted_at is null group by u.id,u.email having count(o.id)>3 order by revenue desc limit 50;
-- After
SELECT
  u.id,
  u.email,
  COUNT(o.id) AS order_count,
  SUM(o.total_cents) / 100 AS revenue
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
  AND o.status <> 'cancelled'
WHERE u.created_at >= '2026-01-01'
  AND u.deleted_at IS NULL
GROUP BY u.id, u.email
HAVING COUNT(o.id) > 3
ORDER BY revenue DESC
LIMIT 50;

The second version takes more vertical space and is unambiguously better. You can see that o.status <> 'cancelled' belongs to the join rather than the filter — which, as described below, is a real semantic distinction and not merely a stylistic one.

The conventions

Uppercase keywords, lowercase identifiers

SELECT u.id FROM users u WHERE u.active = true;

This makes structure legible without syntax highlighting — in a review comment, a chat message, or a terminal.

One major clause per line, contents indented one level

SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT each start a line. Their contents are indented. This is the highest-value rule on this page.

Alias every table, qualify every column

-- Unclear six months from now
SELECT id, email, created_at FROM users JOIN orders ON ...

-- Explicit
SELECT u.id, u.email, o.created_at
FROM users u
JOIN orders o ON o.user_id = u.id

Unqualified columns break with an ambiguity error the moment a matching column is added to the second table. Qualifying costs nothing.

Split join conditions onto their own lines

LEFT JOIN orders o
  ON o.user_id = u.id
  AND o.status = 'paid'

This is what makes the join-versus-filter distinction visible — and that distinction is a correctness issue, not aesthetics.

Trailing commas

SELECT
  u.id,
  u.email,
  COUNT(o.id) AS order_count
FROM users u

Standard SQL does not permit a trailing comma before FROM, so commas go at line end. This also keeps diffs additive: adding a column adds one line.

Align comparison operators within a clause

WHERE u.created_at >= '2026-01-01'
  AND u.deleted_at IS NULL
  AND u.plan       <>  'free'

Optional, and it costs time to maintain — but it makes predicate lists genuinely scannable in long WHERE clauses. Many teams skip this one; it is the least important rule here.

The join mistake formatting prevents

This is the most common SQL bug in practice, and formatting makes it visible.

-- Intent: all users, plus their paid orders if any
SELECT u.id, o.id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid';              -- BUG

-- What actually happens: the WHERE clause filters out every row where
-- o.status is NULL, which is exactly the users with no matching order.
-- The LEFT JOIN has silently become an INNER JOIN.

-- Correct: the condition belongs to the join
SELECT u.id, o.id
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
  AND o.status = 'paid';

When join conditions sit on their own indented lines under ON, a stray condition in WHERE stands out immediately. When everything is on one line, it does not. This is the concrete argument for the indentation rule — it is not decoration.

Formatting CTEs

Common table expressions dramatically improve long queries, and they format consistently:

WITH active_users AS (
  SELECT id, email, created_at
  FROM users
  WHERE deleted_at IS NULL
    AND last_seen_at >= CURRENT_DATE - INTERVAL '30 days'
),
user_totals AS (
  SELECT
    user_id,
    COUNT(*)        AS order_count,
    SUM(total_cents) AS revenue_cents
  FROM orders
  WHERE status = 'paid'
  GROUP BY user_id
)
SELECT
  au.id,
  au.email,
  COALESCE(ut.order_count, 0)   AS order_count,
  COALESCE(ut.revenue_cents, 0) AS revenue_cents
FROM active_users au
LEFT JOIN user_totals ut ON ut.user_id = au.id
ORDER BY revenue_cents DESC;

Name CTEs for what they are (active_users), not what they do (filtered_data). Indent the body one level, close the parenthesis on its own line, and separate CTEs with a blank line.

Formatting is not performance

A beautifully formatted query can still be catastrophically slow. Formatting improves readability and says nothing about execution — that depends on the query plan, which you inspect with EXPLAIN and EXPLAIN ANALYZE.

The structural checks with the highest impact:

  • Indexes supporting every WHERE predicate and JOIN key. Missing indexes are the most common cause of slow queries by far.
  • Select only the columns you need. SELECT * prevents index-only scans, transfers unnecessary data, and breaks callers when the schema changes.
  • Never wrap an indexed column in a function. WHERE DATE(created_at) = '2026-09-18' makes the index unusable; write a range comparison instead: WHERE created_at >= '2026-09-18' AND created_at < '2026-09-19'.
  • Leading wildcards cannot use a B-tree index. LIKE '%term' forces a full scan; use a full-text index or trigram index if you need substring search at scale.
  • Check the join order the planner chose. An unexpected join order usually means stale statistics — run ANALYZE.

Making it stick on a team

A style guide nobody follows is worse than no style guide, because it creates an unspoken expectation that is violated constantly. Three things make it real:

  1. Automate it. A formatter in CI — sqlfluff, pgFormatter, or a language-specific equivalent — removes the debate entirely. Formatting is exactly the kind of decision machines should make.
  2. Never mix formatting and logic in one commit. Run the formatter as its own commit. Otherwise a reviewer cannot tell which changes are real, and the whole benefit of readable diffs is lost.
  3. Put the conventions in the repository. A short SQL_STYLE.md next to the code, referenced from the review checklist, is enough. It does not need to be long — the six conventions above cover most of it.

Start with just two rules if adopting nothing else: one major clause per line, and join conditions on their own lines under ON. Those two deliver most of the value and catch the most common bug.

Our SQL Formatter applies these conventions automatically if you want a quick starting point — but read the output before committing it, since SQL dialects vary and no formatter understands your intent.

Frequently asked questions

No. Whitespace is insignificant to the parser, so formatting affects readability only. Execution depends on the query plan, which you inspect with EXPLAIN.

Uppercase is the dominant convention and makes structure legible without syntax highlighting. The more important thing is consistency within a codebase — pick one and enforce it automatically.

A condition on the right-hand table placed in WHERE filters out the NULL-extended rows, turning the outer join into an inner join. Move the condition into the ON clause.

Decompose with CTEs. A 200-line query is usually several named steps that should be separate CTEs — this improves both readability and often the query plan, since the planner can optimise each part.

In production code, yes: it prevents index-only scans, transfers columns you do not need, and silently breaks callers when a column is added or reordered. It is fine for ad-hoc exploration.