Complex SQL becomes easier to review when formatting, query behavior, and database-specific syntax are examined together. This guide presents a repeatable workflow for using a SQL formatter, tracing joins and filters, finding common correctness bugs, and checking changes before they reach production.
Overview
A formatter can turn a dense SQL statement into a readable layout, but formatting is only the first step. Good SQL review also asks what each relation contributes, where rows are filtered, how NULL behaves, whether aggregation matches the intended grain, and whether pagination is deterministic.
Start by making a safe copy of the query. If you use an online SQL formatter, remove credentials, tokens, personal data, and production-only identifiers before pasting anything. Choose the correct dialect when the tool supports it, because a formatter may accept syntax that another database cannot execute.
For a consistent review, work from the outside of the query inward:
- Format the complete statement without changing its meaning.
- Identify the final result grain: one row per user, order, invoice, event, or another entity.
- Trace tables, joins, filters, grouping, ordering, and pagination.
- Test each logical stage with a smaller query or a temporary limit.
- Review the execution plan separately from the result set.
The SQL Formatter Guide is useful when you need a quick reference for formatting patterns and readability improvements. Formatting should make a query easier to reason about; it should not silently rewrite business logic.
What to track
1. Query structure and result grain
Write down what one output row represents before reviewing individual clauses. A query described as a customer report may actually return one row per customer-order combination after a join. That distinction affects totals, pagination, and application code.
Use indentation to expose the query's hierarchy. Put each selected expression on its own line, align major clauses, and indent subqueries and common table expressions. Give calculated fields and derived tables meaningful aliases. Avoid relying on positional references such as ORDER BY 2 when a named expression is clearer.
2. Joins and accidental row multiplication
For every join, record the intended relationship and expected cardinality. A one-to-many join can multiply rows even when the join condition is correct. Joining customers to orders and then to order items, for example, produces an item-level result unless the query aggregates back to the customer or order level.
To diagnose multiplication, compare counts at each stage:
SELECT COUNT(*) AS rows_after_join,
COUNT(DISTINCT customer_id) AS customers
FROM customer_orders;
If the row count rises unexpectedly, inspect the join keys for duplicates, missing constraints, or an incomplete condition. A join on a non-unique descriptive column is especially worth questioning.
3. Filters and NULL behavior
Separate filters that limit the starting dataset from filters that apply after a join. Moving a condition between ON and WHERE can change an outer join into behavior that resembles an inner join. Test rows with missing related records, because they often reveal this mistake.
Remember that NULL is not equal to zero, an empty string, or another NULL. Use IS NULL and IS NOT NULL for presence checks. Review expressions involving NOT IN, since a NULL in the compared set can produce surprising three-valued logic. When appropriate, compare the behavior of NOT EXISTS and an anti-join against representative data.
4. Aggregation and grouping
Check that every selected expression is either grouped, aggregated, or deliberately supported by the database's rules. More importantly, verify that aggregation happens at the intended level. Summing an order total after joining to line items may count the same order multiple times.
Review conditional aggregates carefully. Decide whether unmatched rows should contribute zero, remain NULL, or be excluded. Explicit expressions such as COALESCE(SUM(amount), 0) can document that decision, but only use them when zero is semantically correct.
5. Ordering and pagination
Pagination needs a stable, deterministic order. Ordering only by a timestamp or status can produce shifting pages when multiple rows share the same value. Add a unique tie-breaker where the database and application design allow it, such as an identifier.
Compare offset pagination with keyset pagination when a result set is large or changes frequently. Offset pagination is straightforward, but later pages may require more work and can shift as rows are inserted or deleted. Keyset pagination usually requires a carefully designed cursor condition and matching index. Validate both correctness and usability before changing the contract exposed to an application.
6. Dialect differences
When moving a query between databases, treat syntax and behavior as separate review items. This compact comparison highlights areas that commonly need verification:
| Area | PostgreSQL | MySQL | SQL Server | SQLite |
|---|---|---|---|---|
| Pagination | LIMIT ... OFFSET | LIMIT ... OFFSET | Often OFFSET ... FETCH with ORDER BY | LIMIT ... OFFSET |
| String concatenation | || | Often CONCAT(); operator behavior depends on settings | + or CONCAT() | || |
| Identifier quoting | Double quotes | Backticks are common; settings affect alternatives | Square brackets or double quotes depending on settings | Double quotes are common |
| Current timestamp | CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | GETDATE() or standard alternatives | CURRENT_TIMESTAMP |
These are practical signposts, not a substitute for the target engine's documentation and version-specific tests. Functions, date arithmetic, regular expressions, upsert syntax, generated columns, and JSON operators deserve particular attention during migration.
Cadence and checkpoints
SQL review is most effective when it happens at several points rather than only when a query fails. Use this cadence as a lightweight team routine:
- During development: format the query, name its result grain, and test joins and filters with representative edge cases.
- During pull request review: compare the before-and-after result counts, inspect changed predicates, and confirm that indexes or plans were considered for expensive paths.
- Before production release: run the query against realistic data volumes, verify permissions and parameter handling, and check the execution plan in a safe environment.
- Monthly or quarterly: revisit recurring reports, dashboards, exports, and scheduled jobs. Data distribution changes can make a previously acceptable plan slow or expose an assumption about uniqueness.
- After schema changes: recheck joins, aliases, nullable columns, indexes, and migration-specific syntax before relying on cached examples or formatter defaults.
Keep a small record of the query's purpose, expected row grain, important parameters, and database dialect. That context is often more valuable than a long comment describing every line.
How to interpret changes
A changed row count is a signal, not automatically a bug. First determine whether the data changed, the query changed, or the database environment changed. Compare counts by stage, inspect a few representative identifiers, and test boundary cases such as empty inputs, missing relationships, duplicate keys, and dates at the start or end of a range.
When performance changes, distinguish elapsed time from the reason for the change. An execution plan may reveal a full scan, an unexpectedly large join, a sort, a spill, or a row estimate that differs substantially from reality. Plan terminology and available commands vary by engine, so capture the plan using the target database's supported tools rather than assuming that one database's EXPLAIN output maps directly to another's.
Do not optimize solely by rearranging lines. A formatter can improve readability without improving execution, and a visually compact query can still be expensive. Check predicates, join selectivity, returned columns, indexes, statistics, and whether the query is asking for more data than the consumer needs.
For debugging workflows that combine SQL with an API, request parameters, or response validation, the API Debugging Runbook provides a useful companion process. If date filters are involved, also verify timezone conversion and boundary handling rather than assuming that a timestamp represents the same instant in every layer.
When to revisit
Revisit this review whenever a query is migrated, a schema changes, a report's numbers look unusual, or an execution time changes materially. Even stable queries deserve a monthly or quarterly checkpoint when they power recurring jobs or operational dashboards.
Use this copy-ready checklist before approving a complex SQL change:
- Have I stated what one result row represents?
- Does each join have an explicit key and expected cardinality?
- Could any one-to-many join multiply rows or totals?
- Have I tested unmatched rows and
NULLvalues? - Do filters appear in the correct
ONorWHEREclause? - Does aggregation occur at the intended grain?
- Is ordering deterministic for pagination?
- Does the syntax match the target database and version?
- Have I checked representative results and an execution plan?
- Is sensitive data excluded from online formatting or debugging tools?
Save the formatted query, the expected result grain, and the last review date with the project. When the next data change, migration, or performance regression arrives, that record turns SQL review from a rushed inspection into a repeatable developer tool workflow.