developer · · Updated

How to Format and Read Messy SQL Queries

By

SQL copied from a log, an ORM's debug output, or a colleague's message usually arrives as one long line. Formatting it isn't about neatness. It's the fastest way to see which condition belongs to which clause, and that's where most query bugs hide. Here's a real example.

The query as it arrived

A report is supposed to list customers in India and the UAE with more than ₹10,000 of paid orders in 2026:

select c.id, c.name, count(o.id) as orders, sum(o.total) as revenue from customers c left join orders o on o.customer_id = c.id and o.status = 'paid' where c.country = 'IN' or c.country = 'AE' and o.created_at >= '2026-01-01' group by c.id, c.name having sum(o.total) > 10000 order by revenue desc limit 20

The numbers for Indian customers look too high, and nobody can see why.

The same query formatted

Run through the SQL Query Formatter with the PostgreSQL dialect:

SELECT
  c.id,
  c.name,
  count(o.id) AS orders,
  sum(o.total) AS revenue
FROM
  customers c
  LEFT JOIN orders o ON o.customer_id = c.id
  AND o.status = 'paid'
WHERE
  c.country = 'IN'
  OR c.country = 'AE'
  AND o.created_at >= '2026-01-01'
GROUP BY
  c.id,
  c.name
HAVING
  sum(o.total) > 10000
ORDER BY
  revenue DESC
LIMIT
  20

With each condition on its own line, the WHERE clause is three lines, and the question to ask becomes obvious: which of these belong together?

The bug: AND binds tighter than OR

SQL evaluates AND before OR, just as multiplication happens before addition. The database reads the WHERE clause as:

c.country = 'IN'
OR (c.country = 'AE' AND o.created_at >= '2026-01-01')

The 2026 date filter only applies to UAE customers. Indian customers are counted across all years of orders, which is exactly why their revenue looked inflated. Whenever AND and OR appear in one clause, write the parentheses explicitly, even when you think the default grouping is right.

The fix, and where join conditions belong

SELECT
  c.id,
  c.name,
  count(o.id) AS orders,
  sum(o.total) AS revenue
FROM
  customers c
  LEFT JOIN orders o ON o.customer_id = c.id
  AND o.status = 'paid'
  AND o.created_at >= '2026-01-01'
WHERE
  c.country IN ('IN', 'AE')
GROUP BY
  c.id,
  c.name
HAVING
  sum(o.total) > 10000
ORDER BY
  revenue DESC
LIMIT
  20

IN (...) removes the OR entirely. The date filter also moved from WHERE into the join's ON clause, and that follows a rule worth memorising for LEFT JOINs:

  • A condition on the joined table in ON filters which orders get matched. Customers with no matching orders are kept, with NULLs for the order columns.
  • The same condition in WHERE runs after the join. For customers with no 2026 orders, o.created_at is NULL, the comparison isn't true, and the customer disappears. Your LEFT JOIN quietly behaves like an INNER JOIN.

In this report, the HAVING clause drops customers without orders anyway, so the placement doesn't change the result. Remove the HAVING to list *every* customer with their 2026 revenue, and the WHERE version silently loses everyone who hasn't ordered yet this year.

What the formatter changes, and what it doesn't

  • Keywords are uppercased (SELECT, LEFT JOIN, AS), so the query's structure stands out from table and column names.
  • Function names stay as you typed them. count and sum remain lowercase in the output above. Only keywords change case.
  • Each clause starts on its own line, and column lists put one item per line. A diff of two versions then shows exactly which column changed.
  • Nothing about meaning changes. Formatting doesn't add parentheses, reorder conditions, or fix anything. It only makes the existing logic visible.

Pick the dialect that matches your database

The formatter has to split the query into tokens before it can indent anything, and each database has its own syntax for quoting names and writing placeholders. With the Standard SQL setting, this SQL Server query fails:

select top 10 [order id], [customer name] from [dbo].[orders] where [status] = @status

The error is Parse error: Unexpected "[order id]", because square-bracket identifiers are SQL Server syntax. Switch the dialect to SQL Server and it formats cleanly, with TOP 10, the bracketed names, and the @status parameter intact. PostgreSQL's $1-style placeholders behave the same way. where id = $1 fails under Standard SQL with a similar parse error, and formats fine with the PostgreSQL dialect selected. The tool offers Standard SQL, MySQL, PostgreSQL, SQLite and SQL Server. Start with the one your query was written for.

Habits that keep queries readable without a tool

  • Give tables short aliases that mean something (c for customers, o for orders) and use them on every column once there's a join.
  • Put each JOIN's conditions directly under it, one per line.
  • Wrap mixed AND/OR logic in parentheses, always.
  • Prefer IN (...) over a chain of ORs on the same column.

Paste your next one-line query into the SQL Query Formatter before you start debugging it. If the query feeds or returns JSON, the JSON Formatter makes the payload side just as readable, and the common JSON errors guide covers what breaks there.

Related guides