SQL formatting conventions, and what they never change

A messy query formatted step by step: keyword case, one clause per line, trailing or leading commas, and why AND/OR precedence still needs parentheses.

On this page

Most SQL arrives as one long line: copied from a log, generated by an ORM, or pasted from a chat. Formatting it is the fastest way to understand it. But formatting has rules of its own, and teams argue about them more than they should. This article formats a real-looking query step by step, explains the conventions that matter and the ones that are taste, and shows the one thing formatting can never fix: a query that means something different from what it looks like.

The outputs below are exactly what our SQL formatter produced.

The starting point

Here is a report query as it might appear in a slow-query log:

select c.name, count(o.id) as orders, sum(o.total) total from customers c left join orders o on o.customer_id = c.id and o.status <> 'cancelled' where c.country = 'VN' or c.country = 'TH' and c.active = true group by c.name having count(o.id) > 2 order by total desc limit 10

And here it is formatted with the default settings (2-space indent, upper-case keywords, trailing commas):

SELECT
  c.name,
  count(o.id) AS orders,
  sum(o.total) total
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id
  AND o.status <> 'cancelled'
WHERE
  c.country = 'VN'
  OR c.country = 'TH'
  AND c.active = TRUE
GROUP BY c.name
HAVING count(o.id) > 2
ORDER BY total DESC
LIMIT 10

The conventions that carry weight

One clause per line. SELECT, FROM, each JOIN, WHERE, GROUP BY, HAVING, ORDER BY and LIMIT start at the left margin. You can now read the query's shape without reading a word: it selects, joins one table, filters, groups and sorts.

Lists one item per line. Three columns become three lines. When someone adds a fourth, the change in a review is one added line instead of an edited one, and a diff checker shows precisely what changed.

Join conditions under the join. ON and every extra AND belonging to the join are indented beneath it. That makes it visible that o.status <> 'cancelled' is part of the join, not the WHERE. This placement matters for a LEFT JOIN: in the ON, it keeps customers without orders; moved to the WHERE, it would silently drop them, turning the left join into an inner one.

Sub-queries indented. A query nested in parentheses is indented one level and its own clauses follow the same rules:

SELECT *
FROM t
WHERE
  (a = 1 AND b = 2 OR c = 3)
  AND d IN (
    SELECT id
    FROM x
    WHERE y = 'it''s'
  )

The conventions that are taste

Keyword case. Upper-case keywords were a necessity before syntax highlighting; today they are a habit many teams keep because SQL in application code is often a plain string with no highlighting. The formatter offers upper, lower or as written. It only changes reserved words: count and sum stay as written, because function and type names are often also column names, and string literals are never touched ('VN' stays 'VN').

Leading or trailing commas. With comma-first, lower case and 4-space indent, the same select list becomes:

select
    c.name
    , count(o.id) as orders
    , sum(o.total) total

The argument for comma-first is that you can comment out any line but the first without breaking the syntax. The argument against is that it reads oddly. Pick one per project and let the tool enforce it.

Indent width. Two spaces, four spaces or a tab. Irrelevant to the database, relevant only to consistency.

Formatting never changes meaning

A formatter only adds or removes whitespace between tokens and changes the case of reserved words. It must never drop, reorder, merge or split a token. Our tool's tests check exactly that: the sequence of meaningful tokens before and after formatting (comments and whitespace removed, keywords in lower case) must be identical. For the query above, both start select, c, ., name, ,, count, (, o and continue identically to the end.

That guarantee cuts both ways. Formatting cannot fix a bug either. Look again at the WHERE clause:

WHERE
  c.country = 'VN'
  OR c.country = 'TH'
  AND c.active = TRUE

It reads like "Vietnam or Thailand, and active". It is not. In SQL, AND binds more tightly than OR, so the database reads it as c.country = 'VN' OR (c.country = 'TH' AND c.active = TRUE): every Vietnamese customer, active or not, plus active Thai customers. The formatter puts OR and AND at the same indentation because it does not restructure your logic. If you mean the other thing, write the parentheses: (c.country = 'VN' OR c.country = 'TH') AND c.active = TRUE, or better, c.country IN ('VN', 'TH') AND c.active = TRUE.

Line comments are dangerous in one-line SQL

A -- comment runs to the end of the line. That is harmless in a formatted file and destructive when someone squashes a query onto one line:

SELECT id
FROM users
WHERE
  status = 'Active' -- only active
  AND created_at >= '2026-01-01'

Joined into one line, everything after -- only active becomes part of the comment and the date filter disappears without an error. The formatter always ends a line comment with a line break for that reason. When you write SQL that may be logged or minified, prefer /* … */ comments.

Common mistakes

  1. Mixing AND and OR without parentheses, then trusting the indentation. Formatting shows the structure you wrote, not the one you meant.
  2. Moving a join filter into WHERE during a tidy-up. With LEFT JOIN, that changes the result.
  3. Expecting quoted identifiers to be normalised. "Order" in PostgreSQL is case-sensitive and different from order; MySQL uses backticks. The formatter keeps every quoted name exactly as written and uses the dialect only to know which quote marks and reserved words to expect.
  4. Ignoring the warnings. An unclosed string such as 'abc is reported with its line and column (line 1, column 8 in select 'abc from t), because everything after it is being read as text.

Paste a query into the SQL formatter before you review it, and paste both versions into the diff checker when you want to see what a colleague actually changed.

Tools mentioned in this article

Back to the blog