Skip to main content

SQL Formatter vs Linter vs Review: What Each One Catches

Tianzhou · Oct 28, 2026

Three tools get called "SQL lint". They see three different things: the text, the statement, the statement plus the target database. A tool catches what it can see, so the question is not which one is better. It is what input each one gets.

Put simply: a formatter is spell-check, a linter is grammar-check, a review is the editor who has read the rest of the book.

Formatter: spell-check

Sees the text. Rewrites whitespace, casing, and comma placement. Never changes what the query does.

Catches: a team of 40 arguing about indentation.

Misses: everything semantic. DELETE FROM users formats beautifully.

Tools: sql-formatter, pgFormatter, SQLFluff fix (which is where the words "format" and "lint" first blur).

Run it in a pre-commit hook and stop the style debate. It buys consistency, not safety.

Linter: grammar-check

Sees the parsed statement. Applies rules to the syntax tree.

Catches: SELECT *, UPDATE without WHERE, a leading % in LIKE, reserved words as identifiers, naming conventions, a MySQL idiom in a Postgres file. Migration-focused linters go further: Squawk flags CREATE INDEX without CONCURRENTLY, ADD COLUMN ... NOT NULL without a default, and DROP COLUMN outright, because those patterns are risky on any table.

Misses: whether this table has two billion rows or two hundred. Whether the column you are dropping is in an index. Whether the column exists in Prod at all. The rule fires on the pattern, not on your database, so it is either noisy on small tables or silent on the case the pattern does not cover.

Tools: SQLFluff (rules per dialect), SQLGlot (parse and transpile, lint-adjacent), Squawk (Postgres migration rules).

Necessary and cheap. It reads the statement, and the statement is not the whole story.

Review: the editor

Sees the statement and the schema of the target database. Some checks also ask the live database.

Catches, beyond what a linter does: ALTER TABLE on a table past a row threshold. Dropping a column that an index depends on. A statement that targets a column Test has and Prod does not. An UPDATE whose EXPLAIN estimate exceeds the affected-row limit.

One example:

UPDATE orders SET status = 'archived' WHERE created_at < '2024-01-01';

It has a WHERE, it names its columns, nothing about the text is an anti-pattern. A statement-only linter cannot tell whether this matches 12 rows or 80 million. A review runs EXPLAIN against Prod and gets an estimate of 80 million rows, over the affected-row limit, with no index on created_at. Against Test the same statement estimates 12 rows. The risk is not in the statement. It is in the table.

How: Bytebase builds an in-memory catalog from the synced schema and applies the statements in order, so statement 3 sees the table statement 1 created. The catalog holds metadata, not rows.

Where it runs: during PR review through CI against the intended target. Configure another check before deployment when needed, because the target can change in between.

Tools: Bytebase SQL Review (100+ rules for MySQL, PostgreSQL, Oracle, SQL Server, and etc; self-hosted). Its naming and statement rules overlap with SQLFluff and Squawk. The schema rules need target-schema metadata; the checks that use EXPLAIN also need a live connection.

Side by side

FormatterLinterReview
SeesTextSyntax treeSyntax tree + target schema
RunsPre-commitCI on the filePR review, per target
Example catchMixed casingCREATE INDEX without CONCURRENTLYA WHERE that matches 80M rows in Prod
Toolssql-formatter, pgFormatterSQLFluff, SQLGlot, SquawkBytebase

Blocking is a setting, not a category. Every finding carries a severity, and whether a severity fails the job is a threshold you choose. Bytebase's CI default reports without failing. The category difference is what the finding is based on.

Which one you need

  1. Solo project or analytics SQL: formatter plus linter. Done.
  2. A shared application schema with migrations: all three. Lint in CI on the file. Review against each environment, because Test and Prod have different schemas and the same migration can pass one and fail the other.

A linter reads your SQL. A review reads your SQL and the database it lands on.

Back to blog

Explore the standard for database governance