Unformatted SQL Is Technical Debt You Pay at Code Review
Poorly formatted SQL doesn't break anything — but it hides bugs, increases review time, and becomes a bad template. How to standardize without ceremony.
You open the PR and the query has 40 columns on a single line. You know what it does. The reviewer doesn't. You'll explain in the comments, the reviewer will approve without understanding, and six months from now nobody will remember why there's a LEFT JOIN there.
This post is about why poorly formatted SQL is real technical debt — and how to solve it without ceremony.
What unreadable SQL hides
Put these two queries side by side:
select u.id, u.name, u.email, o.total, o.created_at from users u left join orders o on u.id = o.user_id where u.active = true and o.total > 100 order by o.created_at desc limit 50
SELECT
u.id,
u.name,
u.email,
o.total,
o.created_at
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE
u.active = true
AND o.total > 100
ORDER BY o.created_at DESC
LIMIT 50
The result is identical. The second takes three extra seconds to write and half an hour less to review.
The difference isn't aesthetic. In the first version, the LEFT JOIN disappears in the noise — and LEFT JOIN matters. It means the query also returns users without orders, with o.total as NULL. If the business expected only users with orders above $100, that's a bug. In the second version, the intent is immediately visible: you see the LEFT JOIN, pause, and ask "is that really what we want here?"
Formatting isn't about looking pretty. It's about making logic auditable.
The conventions that matter in practice
There's no universal SQL standard, but some choices consistently reduce friction:
Keywords in uppercase. SELECT, FROM, WHERE, JOIN — uppercase. Columns and tables in lowercase. This creates immediate visual contrast between structure and data, without needing syntax highlighting.
One column per line in SELECT. When the column list goes beyond three, one per line. This makes git diff easier, makes it easier to comment on a specific column in the PR, and makes it easier to add or remove without touching other lines.
WHERE conditions aligned. Each condition on its own line, with AND and OR at the beginning (not at the end). The reason is simple: commenting out a condition at the beginning of a line is more direct than hunting for the AND at the end of the previous line.
WHERE
u.active = true
AND o.total > 100
-- AND o.status = 'paid' ← easy to comment out during debug
Explicit JOINs, never comma in FROM. FROM users, orders WHERE users.id = orders.user_id is valid in every SQL dialect. It's also the fastest way to accidentally produce a cartesian product when you forget the join condition. Use explicit JOIN, always.
The problem with standardizing manually
The real obstacle isn't knowing the conventions — it's applying them consistently on a team where everyone has a different editor, a different plugin, and a different level of patience for manual formatting.
The solution is to automate. For queries that will go into code (migrations, raw ORM queries, scripts), the ideal is to have a formatter in the pipeline. For day-to-day use — pasting a query in a ticket, sharing in Slack, reviewing before running on a production database — I use the SQL Formatter directly in the browser. Paste the query, choose the dialect (PostgreSQL, MySQL, T-SQL), and it comes out formatted. Especially useful when you receive a query from someone else and need to understand it before anything else.
Formatting in migrations is different
An observation worth separating: SQL in migration files has additional rules beyond formatting.
Migrations are read by humans during review and by machines during execution — and are rarely reverted after being applied in production. This means each migration needs to be self-explanatory without external context. Inline comments are mandatory when the query has any non-obvious logic:
-- Remove duplicate rows keeping the most recent entry per user_id
-- Safe to run: no FK references this table
DELETE FROM user_sessions
WHERE id NOT IN (
SELECT MAX(id)
FROM user_sessions
GROUP BY user_id
);
Clean formatting + intent comment = migration that anyone on the team can audit six months later without asking anything in Slack.
Readability is a team decision, not a personal one
Unreadable SQL is almost never the result of laziness — it's the result of context. The person wrote it directly in the console, tested it, copied and pasted. It works. Nobody will refactor what already works.
The problem is when this pattern replicates. One unformatted query becomes a template for the next. A year from now, the codebase has fifty queries that nobody can review in less than ten minutes each.
The change doesn't need to be radical: automatic formatter in pre-commit for .sql files, convention documented in the README, and the habit of formatting before sharing. The cost is low. The return shows up the first time someone catches a bug in a query during review instead of in production.
- 01 UTF-8 and encoding problems: why accents break and how to fix them What UTF-8 is, code points, mojibake, and why accented text turns to garbage — the mechanism explained end to end.
- 02 MIT, Apache, and GPL Licenses: Practical Differences for Developers MIT, Apache 2.0, and GPL are not the same thing. Understand permissive vs copyleft, patent protection, and compatibility before shipping code to production.