DEVESSENTIALS

SQL Formatting Best Practices: Write SQL That's Easy to Read

SQL that works and SQL that's readable are two different things. Consistent formatting makes queries easier to review in PRs, debug at 2am, and hand off to colleagues. Here are the conventions professionals use.

Keyword Casing

Use uppercase for SQL keywords. This visually distinguishes the SQL language constructs from your table and column names:

-- Good
SELECT
    user_id,
    email,
    created_at
FROM users
WHERE status = 'active'
  AND created_at > '2026-01-01'
ORDER BY created_at DESC;

-- Bad (hard to scan)
select user_id, email, created_at from users where status = 'active'

One Clause Per Line

Each major SQL clause (SELECT, FROM, WHERE, GROUP BY, ORDER BY, LIMIT) starts on its own line. Column lists are indented under SELECT:

SELECT
    o.order_id,
    o.total_amount,
    u.email,
    u.name
FROM orders o
INNER JOIN users u ON u.user_id = o.user_id
WHERE o.status = 'completed'
  AND o.created_at >= CURRENT_DATE - INTERVAL '30 days'
ORDER BY o.total_amount DESC
LIMIT 100;

This layout means you can add, remove, or comment out any column or condition without touching other lines — diffs are clean.

Commas: Leading vs Trailing

Two schools of thought. Leading commas keep errors visible:

-- Leading commas (errors are obvious — a line without comma is wrong)
SELECT
    user_id
  , email
  , created_at

-- Trailing commas (more natural to read)
SELECT
    user_id,
    email,
    created_at

Most style guides prefer trailing commas for readability. Pick one and be consistent. A formatter like the DevEssentials SQL Formatter will enforce your choice automatically.

JOINs

Always use explicit JOINs with ON conditions. Never use implicit joins (comma-separated tables):

-- Good: explicit JOIN, relationship is clear
SELECT
    o.order_id,
    p.product_name
FROM orders o
INNER JOIN order_items oi ON oi.order_id = o.order_id
INNER JOIN products p    ON p.product_id = oi.product_id;

-- Bad: implicit join, easy to accidentally create a cartesian product
SELECT o.order_id, p.product_name
FROM orders o, order_items oi, products p
WHERE oi.order_id = o.order_id
  AND p.product_id = oi.product_id;

Align the ON keyword with the JOIN keyword — some teams go further and align all ON clauses to the same column.

Aliases

Use short, consistent aliases for tables. Initials or abbreviated names work well:

FROM orders o
INNER JOIN users u        ON u.user_id = o.user_id
INNER JOIN order_items oi ON oi.order_id = o.order_id

Always qualify column names with the table alias when joining multiple tables. Never leave ambiguous column references that break when a new table is joined.

WHERE Conditions

Align AND and OR operators at the start of each condition. Use parentheses to make complex logic explicit:

WHERE o.status = 'active'
  AND (
        o.amount > 1000
     OR o.priority = 'high'
      )
  AND o.created_at > '2026-01-01'

Subqueries and CTEs

Prefer CTEs (Common Table Expressions) over nested subqueries — they're named, readable, and easier to debug:

-- Subquery (hard to read at a glance)
SELECT user_id, total
FROM (
    SELECT user_id, SUM(amount) AS total
    FROM orders
    WHERE status = 'completed'
    GROUP BY user_id
) AS order_totals
WHERE total > 500;

-- CTE (clear intent, easy to test each step)
WITH order_totals AS (
    SELECT
        user_id,
        SUM(amount) AS total
    FROM orders
    WHERE status = 'completed'
    GROUP BY user_id
)
SELECT user_id, total
FROM order_totals
WHERE total > 500;

Chain multiple CTEs for complex queries — each CTE builds on the previous one, making the data transformation steps explicit.

NULL Handling

Use IS NULL and IS NOT NULL, never = NULL (which always returns false in SQL):

-- Correct
WHERE deleted_at IS NULL
WHERE email IS NOT NULL

-- Always wrong — NULL != NULL in SQL
WHERE deleted_at = NULL

Comments

Use inline comments to explain non-obvious business logic, not what the SQL syntax does:

-- Good: explains business rule
WHERE created_at > CURRENT_DATE - INTERVAL '90 days'  -- rolling 90-day window per billing policy

-- Useless: explains the syntax
WHERE status = 'active'  -- filter by status

Automate It

Don't rely on manual formatting. Use a formatter in your workflow:

  • Online: DevEssentials SQL Formatter supports MySQL, PostgreSQL, SQLite, T-SQL, BigQuery, MariaDB
  • CLI: sql-formatter (npm), sqlfluff (Python) for CI/CD integration
  • IDE: DataGrip, DBeaver, VS Code SQL extensions all have built-in formatters
  • Pre-commit hooks: run sqlfluff lint before every commit

Format your SQL now with the SQL Formatter — supports MySQL, PostgreSQL, SQLite, T-SQL, BigQuery and MariaDB with configurable indentation.

Frequently Asked Questions

Does SQL formatting affect performance?

No. SQL formatting is purely cosmetic — whitespace, newlines, and keyword casing have zero effect on query execution. The database parser strips all of that before parsing. Formatting affects the humans reading the query, not the database executing it.

Should SQL keywords be uppercase or lowercase?

Uppercase keywords (SELECT, FROM, WHERE) are the traditional convention and are used in most style guides (SQLStyle.guide, GitLab, Kickstarter). The main argument: it visually separates SQL keywords from table/column names, making queries easier to scan. Some modern teams prefer lowercase for less visual noise. Either is fine — the key is consistency within a codebase.

Should I use SELECT * or list columns explicitly?

Always list columns explicitly in production code. SELECT * returns all columns including ones you may not need, increases network payload, breaks if columns are added/removed/reordered, and makes it impossible to understand what data the query actually uses without running it. The only acceptable use of SELECT * is in ad-hoc exploratory queries, never in application code.

Where should I put JOIN conditions vs WHERE conditions?

JOIN conditions (how tables relate) go in the ON clause. Filter conditions (which rows you want) go in the WHERE clause. Mixing them with implicit joins (comma-separated tables with all conditions in WHERE) makes queries hard to read and is a common source of accidental cartesian products. Explicit JOINs with ON conditions are unambiguous about intent.