面向代码审查和版本控制的 SQL 格式化

本页尚未翻译成中文,因此以英文显示。

How consistent SQL formatting keeps diffs small and code reviews focused on what matters: agreeing on one format, separating reformatting from logic changes, keeping SQL in versioned files, and a checklist for reviewing SQL in pull requests.

SQL often gets less review attention than application code. It hides in strings, in migration folders and in report definitions, and when it does show up in a pull request, the diff is frequently dominated by whitespace and line breaks. This guide is about making SQL changes as easy to review as any other code, so that the reviewer's attention goes to the logic and not to the layout.

Why formatting noise hurts reviews

A diff tool compares lines. If one developer writes a query on four long lines and the next one reformats it into twenty short ones while also changing a condition, the diff shows every line as changed. The real change is somewhere in there, and the reviewer has to find it by reading both versions in full. Here is a small example where a query was reformatted and one value was changed at the same time:

--- a/reports/late_orders.sql
+++ b/reports/late_orders.sql
@@ -1,4 +1,7 @@
-select o.order_id, o.customer_id, o.created_at
-from orders o
-where o.status = 'open' and o.created_at < current_date - 14
-order by o.created_at
+SELECT o.order_id,
+    o.customer_id,
+    o.created_at
+FROM orders o
+WHERE o.status = 'open'
+AND o.created_at < CURRENT_DATE - 7
+ORDER BY o.created_at

Every line is marked as changed, yet only one thing changed in meaning: the threshold went from 14 days to 7. In a four-line query a careful reviewer may notice. In a two-hundred-line report query, it will very likely slip through. Formatting noise has other costs as well:

  • git blame attributes every line to the person who reformatted it, so the history of why a condition exists is one extra step away.
  • Merge conflicts become more likely and harder to resolve, because two branches that touched the same query in different styles conflict on every line.
  • Reviewers learn to skim SQL diffs, which is exactly the habit you do not want for code that can update or delete data.

Agree on one format

The fix is to make formatting boring: everybody produces the same layout for the same query, so diffs only show real changes. That requires two things.

First, a decision. The team agrees on indentation, line length, comma placement, keyword case and how parentheses are laid out. The specific choices matter much less than having made them; the SQL style guide discusses the trade-offs, and leading vs trailing commas covers the most debated one in depth.

Second, a tool that applies the decision mechanically. Style rules that rely on people remembering them drift within weeks. A formatter with fixed options produces the same output from the same input every time, regardless of who runs it, which is what makes diffs stable.

Using the formatter's options as a shared standard

SQL Format has five options, and together they define the whole layout:

OptionDefaultWhat it controls
Indent spaces4Spaces per indentation level; tabs in the input are replaced by spaces, except inside string literals and quoted identifiers.
Maximum columns120Lines longer than this are wrapped; 0 disables wrapping.
CommasafterTrailing (after) or leading (before) commas in lists.
KeywordsupperUppercase or lowercase keywords.
Parentheses on new lineoffWhether an opening parenthesis that starts an indented block goes on its own line.

Pick values once and write them down in the repository, for example in the README, in a contributing guide, or as a short comment block in a file next to your SQL:

-- SQL formatting standard for this repository
-- Formatter: sql-format.io (paste, format, commit the result)
--   Indent spaces             = 4
--   Maximum columns           = 120
--   Commas                    = after (trailing)
--   Keywords                  = upper
--   Parentheses on new line   = off
-- Reformatting-only changes go in their own commit.

With the values recorded, anyone can reproduce the standard layout: paste the SQL into the formatter, set the documented options, format, and paste the result back. Because the output depends only on the input and the options, two people formatting the same query get the same text. SQL Format is a web page, not a command-line tool or a build plugin, so this step happens by hand before committing, and the review checks that it was done. For SQL that contains sensitive logic, it may help to know that the formatter does not store the SQL you submit; the privacy policy has the details.

Reformat in a separate commit

The single most effective habit: never mix reformatting and logic changes in the same commit. If a file needs reformatting before you change it, do it in two steps:

  1. Reformat the file and commit with a message such as "Reformat open_orders.sql, no logic change". The reviewer can confirm quickly that the change is layout only, or skip it entirely if the team trusts the formatter.
  2. Make the logic change and commit it separately. The diff now shows only the lines that matter.

When adopting a format for an existing codebase, the same rule applies at a larger scale. Reformat everything in one dedicated pull request, merge it quickly to minimize conflicts with open branches, and then require formatted SQL from that point on. Git can also skip such commits when computing blame: list their hashes in a file such as .git-blame-ignore-revs and point the blame.ignoreRevsFile setting at it. Some hosting platforms read that file automatically in their blame view.

Formatting changes layout, never meaning. Even so, after reformatting a large file, run the queries or the test suite once before committing. It is a cheap check that nothing was pasted into the wrong place.

Keep migrations and views in files

SQL that lives only in the database, such as a view created by hand in production, or in a wiki page, has no history and no review. Keep every schema change and every view, function and important report query in a file under version control:

  • Migrations: one file per change, with a name that sorts in order (for example a version number prefix, as Flyway uses). Never edit a migration that has already been applied in a shared environment; write a new one.
  • Views and functions: keep the current definition in a file, ideally as CREATE OR REPLACE where your database supports it, so a change shows up as a normal diff of the definition.
  • Report and batch queries: one query per file is easier to review, diff and test than queries assembled from strings inside application code.

A view definition kept in a file and formatted once gives clean diffs every time the definition changes:

格式化前
create or replace view open_order_summary as select c.customer_id, c.customer_name, count(o.order_id) as open_orders, sum(o.total_amount) as open_amount from customers c join orders o on o.customer_id = c.customer_id where o.status = 'open' group by c.customer_id, c.customer_name;
格式化后(由 SQL Format 处理)
选项: 缩进空格 4 · 最大列数 120 · 逗号: 后 · 关键字: 大写 · 括号另起一行: 否
CREATE OR REPLACE VIEW open_order_summary AS
SELECT c.customer_id,
    c.customer_name,
    COUNT(o.order_id) AS open_orders,
    SUM(o.total_amount) AS open_amount
FROM customers c
    JOIN orders o
        ON  o.customer_id = c.customer_id
WHERE o.status = 'open'
GROUP BY c.customer_id,
    c.customer_name;

With one column per line, adding a column to this view is a one- or two-line diff, depending on the comma style. A migration file should also explain itself: a header comment saying what the change does and how existing rows are handled saves the reviewer a lot of guessing.

-- V42__add_currency_code_to_orders.sql
-- Adds the order currency. Existing rows are backfilled with 'USD'.

ALTER TABLE orders ADD COLUMN currency_code CHAR(3);

UPDATE orders
SET currency_code = 'USD'
WHERE currency_code IS NULL;

ALTER TABLE orders ALTER COLUMN currency_code SET NOT NULL;

The migration formatting checklist goes through the points to check before a migration is merged.

A pull request checklist for SQL

Formatting removes the noise; these are the things a reviewer should then look for. They apply to migrations, data fixes and application queries alike.

  • Explicit column lists. No SELECT * in application code or views, and no INSERT without a column list. Both silently change behavior when a column is added or reordered.
  • A WHERE clause on every UPDATE and DELETE. Check that it is there and that it matches exactly the rows intended. For data fixes, ask for the expected row count and the query used to get it.
  • Index impact. New filters, joins and sort orders on large tables may need an index. New indexes have a cost too: they slow down writes, and building one on a large table can lock it or take a long time, depending on the database and the options used.
  • Transaction boundaries. Statements that must succeed or fail together belong in one transaction. Long-running statements inside a transaction hold locks for its whole duration. Know whether your migration tool wraps each file in a transaction, and whether your database allows DDL inside one.
  • Locking and size of data changes. An update of millions of rows in a single statement can block other work and grow the transaction log. Large backfills are often safer in batches.
  • NULL handling. Conditions such as column <> 'x' do not match rows where the column is NULL. Check that this is intended.
  • Formatting and naming. The SQL follows the documented options, and new tables, columns and constraints follow the naming conventions.

A data fix written to be reviewable shows its own evidence: a count of the affected rows, the change itself with a precise WHERE, and an explicit transaction so the person running it can compare counts before committing.

BEGIN;

-- Expected: about 1,200 rows (checked with the SELECT below on a recent copy of production)
SELECT count(*)
FROM orders
WHERE status = 'pending'
AND created_at < DATE '2026-09-01';

UPDATE orders
SET status = 'cancelled',
    updated_at = now()
WHERE status = 'pending'
AND created_at < DATE '2026-09-01';

-- Compare the reported row count with the SELECT above, then:
COMMIT;

Summary

  • Agree on one format and record the five option values in the repository.
  • Format SQL before committing, so diffs only show real changes.
  • Keep reformatting in its own commit, separate from logic changes.
  • Keep migrations, views and important queries in versioned files.
  • Review SQL for column lists, WHERE clauses, index impact and transaction boundaries, not for layout.

For the layout of large queries that show up in reviews, see how to format complex queries, and for the habits reviewers most often flag, common SQL readability mistakes.