← All Articles
SQL

Why SQL Formatting Matters in Code Reviews

SQL has a reputation as the language developers write but never bother to format. Application code goes through linters, formatters, and style guides. SQL — which often controls the most critical business logic in an application — gets pasted as a single line into a string constant and committed. This is a problem, and it shows up most painfully during code reviews when a reviewer has to decipher what a single unbroken line of SQL actually does — the 235-character query below is a fair sample — before they can evaluate whether it is correct.

What unformatted SQL looks like in a pull request

Here is a real-world style query as it typically appears when generated by an ORM or written in a hurry:

SELECT u.id,u.name,u.email,o.order_id,o.total,o.status FROM users u INNER JOIN orders o ON u.id=o.user_id WHERE u.created_at > '2024-01-01' AND o.status IN ('pending','processing') AND o.total > 1000 ORDER BY o.created_at DESC LIMIT 50

It is technically correct. But is it? Take a moment to verify. Can you tell at a glance whether the JOIN is correct? Whether the WHERE conditions use the right columns? Whether LIMIT makes sense for this query? Now look at the same query formatted:

SELECT
    u.id,
    u.name,
    u.email,
    o.order_id,
    o.total,
    o.status
FROM
    users u
INNER JOIN
    orders o ON u.id = o.user_id
WHERE
    u.created_at > '2024-01-01'
    AND o.status IN ('pending', 'processing')
    AND o.total > 1000
ORDER BY
    o.created_at DESC
LIMIT 50

The logic is immediately readable. The JOIN condition is on its own line. Each WHERE clause is distinct. The SELECT columns are enumerated clearly. A reviewer can now evaluate the query's correctness in seconds rather than minutes.

Bugs that formatting reveals

Formatting is not just cosmetic — it actively exposes logic errors that are invisible in minified SQL. The most common example is operator precedence in WHERE clauses. Consider:

-- Minified — ambiguous intent
WHERE status = 'active' AND role = 'admin' OR role = 'superuser'

-- Formatted — still ambiguous, but the problem is now visible
WHERE
    status = 'active'
    AND role = 'admin'
    OR role = 'superuser'
    -- ↑ This returns ALL superusers, active or not! Missing parentheses.

-- Fixed
WHERE
    status = 'active'
    AND (role = 'admin' OR role = 'superuser')

The AND/OR precedence bug in the first version is nearly invisible when it's all on one line. The formatted version makes the missing parentheses an obvious problem.

SQL formatting conventions worth adopting

Uppercase keywords

SQL keywords (SELECT, FROM, WHERE,JOIN, GROUP BY, etc.) in uppercase makes them visually distinct from table names, column names, and aliases. This is a long-standing and common convention.

One column per line in SELECT

Listing each selected column on its own line makes it easy to add, remove, or comment out individual columns. In a code diff, adding a column shows as a single added line rather than a change to a long CSV.

Leading commas vs trailing commas

Teams are divided on this. Trailing commas (col1, col2, col3 each on their own line) are more familiar for developers. Leading commas (, col1) make it easier to comment out the last column without editing the previous line. Pick one and be consistent.

AND/OR at the start of each line

Putting AND and OR at the beginning of each WHERE clause line (rather than the end) makes it easy to comment out individual conditions during debugging:

WHERE
    u.active = true
    AND u.verified = true
    -- AND u.premium = true  ← easy to comment out for testing
    AND o.created_at > NOW() - INTERVAL '30 days'

CTEs make complex queries readable

Common Table Expressions (WITH clauses) are the SQL equivalent of named variables. They break a complex query into named, readable steps — turning a deeply nested subquery into a sequence of named transformations that a reviewer can follow step by step.

-- Before: deeply nested subquery
SELECT u.name, order_totals.total
FROM users u
JOIN (SELECT user_id, SUM(total) as total FROM orders WHERE status = 'completed' GROUP BY user_id) order_totals
ON u.id = order_totals.user_id;

-- After: readable CTE
WITH completed_orders AS (
    SELECT
        user_id,
        SUM(total) AS total
    FROM orders
    WHERE status = 'completed'
    GROUP BY user_id
)
SELECT
    u.name,
    co.total
FROM users u
JOIN completed_orders co ON u.id = co.user_id;

How to enforce SQL formatting in a team

  • Pre-commit hooks: Use sqlfluff (Python, open source) as a pre-commit hook to lint and format SQL files before they can be committed.
  • CI pipeline check: Add sqlfluff lint to your CI pipeline to block merges with improperly formatted SQL.
  • IDE plugins: Most major IDEs have SQL formatting plugins. IntelliJ IDEA Ultimate has built-in SQL formatting as part of Database Tools and SQL — that support is limited in the Community edition, so do not assume everyone on the team has it. DataGrip uses the same engine. VS Code has the SQL Formatter extension.
  • Code review checklist: Add SQL formatting to your pull request checklist template so reviewers know to flag it.

Formatting ORM-generated SQL for debugging

When debugging a slow query in a Spring Boot application with Hibernate, enable SQL logging with spring.jpa.show-sql=true and spring.jpa.properties.hibernate.format_sql=true. This outputs formatted SQL to the log, making it much easier to understand what query Hibernate is generating. You can then copy that SQL and run it directly in your database client to analyse its execution plan.

Try it in the browser