Data

SQL Formatting Tips for Cleaner Queries and Easier Reviews

Readable SQL helps teams debug data issues faster and review changes with far less mental overhead.

Readable SQL helps teams debug data issues faster and review changes with far less mental overhead.

This article focuses on the practical side of the workflow, including where developers usually get stuck, what to verify before trusting the result, and how the topic shows up in day-to-day implementation work.

Why Formatting Is the First Step, Not the Last

Most developers treat SQL formatting as a cosmetic afterthought, something you might tidy up if there is time before a pull request goes out. That is backwards. A query you cannot read is a query you cannot reason about, and a query you cannot reason about is one where bugs hide comfortably. Before you ever think about adding an index or rewriting a join, you should be able to see the shape of the statement at a glance.

Consider a real example. A teammate pastes a single 400-character line that selects eight columns, joins four tables, filters on three conditions, and groups by two of them. To understand it, you scroll horizontally, count commas, and mentally rebuild the structure. Reformatted onto multiple lines with each clause on its own row, the same query reveals in seconds that one join has no condition and one filter is comparing a date column to a string. The logic did not change. Your ability to see the logic did.

This is the core argument of this article: format first, then review, then optimize. Clean formatting is not about aesthetics for their own sake. It is the cheapest tool you have for surfacing logic errors, accidental cartesian products, and misplaced filters before they reach production.

Putting Each Major Clause on Its Own Line

The single highest-value formatting habit is giving each top-level clause its own line: SELECT, FROM, JOIN, WHERE, GROUP BY, HAVING, and ORDER BY. When these keywords start at the left margin, or at a consistent indent, the reader gets a vertical table of contents for the query. You can scan down the left edge and immediately know what the statement does without parsing a word of the column list.

Within the SELECT list, put each column on its own line and align them. Leading commas versus trailing commas is a genuine team debate, and either works, but pick one. Leading commas make it obvious when a column is missing its separator and produce cleaner diffs when you add a column at the end, since the previous last line does not change. Trailing commas read more naturally to most people. The point is consistency, not which camp wins.

A small but real example: writing four columns on one line hides the fact that one was added by someone who forgot the comma, silently fusing two column names into an accidental alias rather than two columns. Stacked vertically, that mistake is glaring. Horizontal density is where these errors survive code review.

Formatting Joins So Relationships Are Obvious

Joins are where the most expensive mistakes live, so they deserve the most disciplined formatting. Always write the join type explicitly (INNER JOIN, LEFT JOIN) rather than relying on a bare JOIN, and put the ON condition on the line directly below its join, indented one level. When a join has multiple conditions, stack each on its own line connected by AND. This makes it trivial to confirm that every join key is actually present.

The classic disaster is the accidental cartesian product. Imagine three tables listed comma-separated in the old style, with the WHERE clause meant to tie them together a dozen lines below. If someone forgets one of the join predicates, you do not get an error, you get a result set multiplied by the row count of the unjoined table. With explicit JOIN ... ON syntax and each ON aligned under its join, a missing condition is visually impossible to miss. Migrating away from comma joins is itself a formatting decision that prevents this entire class of bug.

When you review a query, read the joins first. Confirm each ON references both tables it should, check whether a LEFT JOIN should really be an INNER JOIN, and watch for join conditions that quietly act as filters. A region condition living in a LEFT JOIN ON clause behaves very differently from the same predicate in WHERE, and clean formatting is what lets you notice the distinction.

Indenting Subqueries and Common Table Expressions

Nested subqueries are where indentation earns its keep. A subquery inside a FROM or WHERE clause should be indented as a self-contained block, with its own SELECT, FROM, and WHERE lined up internally just as the outer query is. The opening parenthesis can sit at the end of the line that introduces the subquery, and the closing parenthesis should align with the start of that block so the eye can match them.

Better still, prefer common table expressions (WITH clauses) over deeply nested inline subqueries when your database supports them. A query nested three levels deep reads inside-out and forces you to hold context in your head. Broken into named CTEs, each step gets a meaningful name, sits at a flat indentation level, and reads top to bottom like a recipe. A triple-nested SELECT is a puzzle; a chain of named CTEs tells a story.

The readability win here also has a review benefit. When each logical stage is a named CTE, a reviewer can validate one stage at a time and even run it in isolation. Nested subqueries cannot be tested piecemeal without surgery, so reviewers tend to skim them, which is exactly how logic errors slip through.

Making CASE Expressions Legible

CASE expressions cram conditional logic into a single value, and when written on one line they become impenetrable. Put CASE on its own line, indent each WHEN ... THEN on a separate line beneath it, place ELSE on its own line, and align END with the original CASE. This turns a wall of keywords into something that reads like an if-else ladder, which is exactly what it is.

The benefit is not only readability but correctness. CASE evaluates conditions top to bottom and stops at the first match, so ordering matters. When every WHEN sits on its own line, an overlapping or unreachable condition becomes visible. A common bug is putting a WHEN for amount over 100 before a WHEN for amount over 1000, which means the second branch can never fire. Stacked vertically with the comparisons aligned, that mistake jumps out; buried in a one-liner, it ships.

Watch for the silent ELSE NULL too. A CASE without an ELSE returns NULL for unmatched rows, which is easy to overlook when the whole expression is compressed onto a single line. Formatting the CASE so the missing ELSE is conspicuously absent prompts the reviewer to ask whether NULL is really the intended default.

Where Filters Belong and Why It Matters

WHERE clauses deserve the same one-condition-per-line treatment as joins, with each predicate connected by AND or OR on its own line. The moment you mix AND and OR, add explicit parentheses and indent the grouped conditions, because operator precedence is a frequent source of subtle bugs. A WHERE clause that mixes an AND and an OR without parentheses almost certainly does not do what the author intended, and only clear formatting reveals the ambiguity.

Filter placement is a review checkpoint, not just a style choice. As mentioned with joins, a condition on the right-hand table of a LEFT JOIN belongs in the ON clause if you want to preserve unmatched left rows, and in WHERE if you want to filter them out. Putting it in the wrong place turns your LEFT JOIN into a de facto INNER JOIN. When filters are formatted clearly and grouped by intent, this kind of mistake is easy to catch during review.

Also scan for filters that defeat indexes once correctness is confirmed. Wrapping a column in a function, like filtering on YEAR(created_at) = 2026, prevents an index on created_at from being used; a range with created_at greater than or equal to one date and less than the next is both index-friendly and, when each bound is on its own line, perfectly readable. Formatting first makes these performance reviews faster.

Agreeing on Team Conventions and Automating Them

Individual formatting taste is endless and unwinnable as a debate, so the practical answer is a written team convention plus a tool that enforces it. Decide the handful of things that actually matter: keyword case (uppercase keywords against lowercase identifiers is a popular and readable choice), indent width, leading versus trailing commas, and where the comma sits in the SELECT list. Write it down in a short style guide and stop relitigating it in every pull request.

Then automate it. Formatters like sqlfluff, which both lints and fixes, sql-formatter, or your IDE's built-in formatter can apply the agreed rules on save or in a pre-commit hook. Add a lint step to continuous integration so badly formatted SQL fails the build before a human ever looks at it. The goal is that reviewers spend their attention on whether the join is correct and the filter is in the right clause, never on whether someone used tabs or spaces.

A subtle payoff of consistent formatting is cleaner version-control diffs. When everyone formats the same way, a one-line logic change shows up as a one-line diff instead of being buried in whitespace churn, and the history points to the person who actually changed the logic rather than the last person who reformatted the file.

Balancing Readability Against Performance

There is a persistent myth that clean, readable SQL is somehow slower than tightly packed SQL. It is not. Whitespace, line breaks, indentation, and keyword case have zero effect on the execution plan the optimizer produces. The database parses your statement into the same internal representation whether it arrived as one line or fifty, so formatting is a free improvement with no runtime cost.

That said, readability and performance are different concerns that should be tackled in order. First make the query correct and clear. Then, with the query readable, profile it with EXPLAIN or EXPLAIN ANALYZE and address the genuine bottlenecks: missing indexes, full table scans, redundant joins, or a subquery that should be a join. Trying to optimize a query you cannot read leads to guesswork and to changes that accidentally alter results.

Occasionally the most performant form of a query really is less intuitive, perhaps a window function replacing a correlated subquery, or a deliberate denormalization. When that happens, keep the query formatted cleanly and add a short comment explaining why the non-obvious approach was chosen and what it replaced. Future maintainers, including you, will read that comment and avoid innocently rewriting it back into the slow, prettier version. Clean formatting plus a sentence of context is what makes hard-won optimizations survive the next code review.