SQL Formatter — Read 200-Line Query Before Blaming ORM

(Updated: July 16, 2026 ) SQL ORM code-review sql-formatter

An ORM log dumps a 200-line JOIN as a single unbroken string. Someone blames “Postgres being slow” or “the ORM being dumb.” Half the time the query is doing exactly what the code asked — including an accidental cross join or a filter applied in memory after fetching too much. Format first, accuse second.

What pretty-printing buys you in review

  • JOIN graph — You can see which tables connect and whether an ON clause is missing (cartesian explosion).
  • Filter placementWHERE versus ON mistakes become obvious.
  • Select listsSELECT * across five joins shows up as a wall of columns.
  • Subqueries / CTEs — Nesting depth is visible; correlated subqueries stop hiding.
  • ORDER BY / LIMIT — You notice sorts without indexes or limits applied too late in a wrapper.

Formatting does not make SQL faster. It makes wrong SQL embarrassing enough to fix before you rewrite the data layer.

A review ritual that works

  1. Reproduce the slow endpoint once with query logging enabled.
  2. Copy the heaviest query into a formatter.
  3. Read top-down: FROM/JOIN → WHERE → GROUP/aggregate → ORDER/LIMIT.
  4. Estimate rows at each step (EXPLAIN when needed).
  5. Only then change ORM code, indexes, or caching.

Paste monsters into sql-formatter when the log line is unreadable — then drop the pretty version into the PR comment so reviewers share the same picture. Diffing two formatted queries beats arguing about two one-line blobs from the slow-query log.

Patterns formatters help you spot

SmellLooks like after formatLikely fix
Missing join predicateFROM a, b or JOIN without ONAdd relationship; avoid Cartesian
Filter in appHuge result, tiny usagePush predicate into WHERE
Over-fetchDozens of columns selectedExplicit select / partial entity
Duplicate joinsSame table aliased thriceReshape query or use EXISTS
OR across columnsHard to indexRewrite, union, or generated column
Late LIMITSort of a million rows, keep tenPush limit earlier or prefilter

ORM-specific notes

  • Eager load everything — Pretty SQL shows the join farm; maybe you wanted two queries.
  • N+1 — Logs show many tiny queries; formatting each matters less than counting them and batching.
  • Raw SQL fragments — Still format before review; string-built SQL hides typos and injection mistakes.
  • Soft-delete global scopes — Formatted SQL reveals surprise deleted_at IS NULL predicates that change plans.

EXPLAIN after you can read it

Once the query is legible:

EXPLAIN (ANALYZE, BUFFERS)
SELECT ...

Look for sequential scans on large tables, huge row estimates, and sorts spilling to disk. A formatted query makes mapping EXPLAIN nodes back to clauses faster — especially when aliases collide in the unreadable original.

Share the formatted query in the ticket

Paste the pretty SQL into the incident doc with parameter values redacted (replace literals with $1-style placeholders if needed). Future you will thank present you when the same endpoint regresses after a “small” model change. Include the approximate row counts from EXPLAIN, not only the text.

What not to debate

  • Uppercase versus lowercase keywords after the team formatter owns it
  • Whether CTEs are “always bad” without measuring
  • Reformatting generated migrations by hand every time
  • Rewriting the ORM before anyone has read the SQL

Spend energy on predicates, indexes, and data model. Use formatting as the on-ramp. If nobody has read the SQL, you do not yet know whether you need a new ORM, a new database, or a missing WHERE clause. Pretty-print the offender, circle the join that fans out rows, and fix the small thing when it is small. Blame the ORM only after the query’s intent is visible on screen.