Getting SQL to run is one thing. Getting other humans to read it is another. Formatting is the latter, not the former. Most developers’ formatting habit is “manually pressing Tab to adjust indentation in the editor” — that solves 30% of the problem. The remaining 70% (dialect differences, keyword case, semicolons inside strings, comments being swallowed, CTE not recursing, subqueries not knowing when to break line) cannot be solved with manual Tab presses.
A SQL formatter’s core value is not “making SELECT look pretty” — it is making queries human-readable during code review, post-mortem analysis, and slow-query log inspection. That is infrastructure, not beautification. This article covers 8 real foot-guns in the “symptom + why + how to fix” format, then introduces the Piick SQL Formatter — 5 dialects, 3 keyword case modes, 4 indent modes, leading/trailing comma, syntax highlighting, click-to-jump error positioning, 5-metric stats, 100% local processing, all done in 1 minute.
30-Second Overview
-
SQL formatter != beautifier: the former is readability infrastructure for query audits / code review / slow-query log analysis; the latter is just “make the code look tidy”
-
Dialect decides everything: the same
SELECT * FROM "my column"is a string in MySQL, a quoted identifier in PostgreSQL, and a syntax error in T-SQL. A formatter without dialect awareness will inevitably rewrite and break things -
8 common foot-guns: dialect mismatch, mixed keyword case, semicolons inside strings triggering false splits, naive formatter swallowing comments, identifier quotes auto-rewritten, non-recursive WITH CTE formatting, all subqueries inlined instead of derived tables getting their own lines, minified SQL losing all structure for debugging
-
Fix direction: always use a formatter with dialect options, never auto-rewrite quotes, always treat comments / CTEs / derived tables as first-class citizens
-
Use the Piick SQL Formatter online to format / minify / beautify — 5 dialects, syntax highlighting, click-to-jump error positioning, 100% local processing, safe to paste production DDL
8 Common Foot-Guns
Foot-Gun 1: Dialect Mismatch (MySQL Backtick Runs on PG and Breaks)
Symptom: A PostgreSQL query, run through a MySQL-style formatter, comes out with all "my column" rewritten as `my column`. Paste it back into psql and you get ERROR: column "my column" does not exist (because PG uses double quotes for identifiers, and backticks are not legal syntax).
Why: Each dialect’s identifier-quote character is different — MySQL uses ` backtick, PostgreSQL / SQLite / Standard SQL use " double quote, T-SQL uses [ ] brackets or double quotes. A naive formatter looks at a string and treats it as a string, never considering dialect context, so it rewrites a PG identifier under MySQL’s rules into a backtick.
Fix: The formatter MUST have a dialect selector, and the dialect decides 2 things — (a) the keyword recognition set (~120 reserved words, different per dialect), and (b) the identifier-quote character. The Piick SQL Formatter switches between 5 dialects; paste PG SQL and select PostgreSQL, and the formatter will not touch your quotes.
Real scenario: When migrating PG → MySQL, a naive reverse operation rewrites PG’s "my column" to MySQL’s `my column`, but PG’s JSONB column becomes MySQL’s JSON, PG’s SERIAL becomes MySQL’s AUTO_INCREMENT — that’s not something the formatter can fix (you have to manually rewrite the schema). But quote-rewriting “syntax errors caused by the formatter itself” must be zero tolerance.
Foot-Gun 2: Mixed Keyword Case (Grep Cannot Find Clauses)
Symptom: During code review, git blame shows a SELECT like Select * From users Where id = 1. You want to grep all WHERE clauses in the codebase with the regex \bWHERE\b (case-sensitive) — zero matches. Switch to -i, and you find 70% lowercase where and 30% uppercase WHERE, because 5 formatter templates use UPPER, 3 use lower, and the rest ignore it.
Why: When the formatter does not enforce keyword case, developers write whatever their editor config (camelCase, snake_case) suggests, and the codebase ends up with 4 case styles mixed — Select / SELECT / select / sElEcT (yes, people really write that). SQL keywords are case-insensitive, so semantics work, but code review, grep, and AST analysis tools all break.
Fix: The formatter should offer a keyword-case option (UPPER / lower / Preserve), the team should lock one style in an ESLint-style config file, and CI should run the formatter to enforce it automatically. The Piick SQL Formatter switches between 3 case modes — pick UPPER and all keywords become SELECT FROM WHERE; pick Preserve and original casing is kept (good for projects that already have an established style).
Real scenario: When accepting a PR in an open-source project, a new contributor mixes case; the reviewer comments “please run the formatter before submitting”; the contributor installs the project’s recommended prettier-plugin-sql and runs it, which reformats 200 unrelated lines, and the reviewer has to review again. Case unification should happen once before commit, not repeatedly during review.
Foot-Gun 3: Semicolons Inside Strings Trigger False Splits (Entire SQL Gets Shattered)
Symptom: An INSERT VALUES contains dirty data with ; inside a string from user input (for example notes = 'Error: connection refused; retry';). The naive formatter splits by ;, generating 2 pseudo-statements; paste it back to execute, the second “statement” is garbage characters and the database throws a syntax error.
Why: The naive formatter uses src.split(';') to split multi-statement SQL, completely ignoring quote state — ; inside a string is textually indistinguishable from a statement terminator. The correct approach is for the tokenizer to first recognize quotes / comments / nesting, then split on ;, only cutting in non-string / non-comment state.
Fix: The first gate of any SQL processing tool is the tokenizer. A naive split(';') works in a demo, but breaks the moment production data (with ;-containing user input) arrives. The tokenizer in the Piick SQL Formatter is character-level, recognizing '...', "..." (PG identifier), `...` (MySQL identifier), nested /* ... */ block comments, -- ... line comments, and MySQL # ... line comments — within all these contexts ; is never a statement terminator.
Real scenario: A data migration script exports INSERTs from CSV; the CSV contains ; in user input as legitimate business data (e.g., SQL statement text fields, CSV description fields). The naive script breaks immediately; the correct approach is to use a tool with a tokenizer to split, or use the COPY protocol (don’t assemble INSERTs).
Foot-Gun 4: Comments Silently Swallowed by Naive Formatter (WHERE Conditions Disappear)
Symptom: SELECT * FROM users WHERE active = 1 -- only active users — after the naive formatter runs, the comment is gone, leaving SELECT * FROM users WHERE active = 1, which looks fine. But you review the git diff and see the comment was silently removed — the comment was a business explanation for “why this WHERE clause is written this way”, and once deleted, you 6 months later have no idea why active=1.
Why: Many formatters fuse the two steps “format = delete comments + re-layout tokens”. Comment tokens don’t affect semantics, so they get thrown away as garbage. But comments are key evidence of schema evolution (“why this field is hardcoded to 1 not a config”, “why this deprecated table is joined”), and deleting them equals deleting project history.
Fix: The formatter must treat comment tokens as first-class citizens, preserving position and content; only minify mode can delete them (because minify is for production execution, not human reading). The Piick SQL Formatter preserves -- line comments and /* */ block comments (including nested ones) in format mode, and only deletes comments in “Minify” mode.
Real scenario: During slow-query log analysis, you see SELECT * FROM orders WHERE created_at < '2020-01-01' -- pre-archive temp query, delete after cleanup; after the formatter runs once, the comment is gone; you 6 months later look at this SQL and don’t know “why created_at is hardcoded to 2020-01-01 — bug or temp query?” — you have to ask a colleague. Comments are the project’s time machine; deleting them equals deleting project history.
Foot-Gun 5: Identifier Quotes Auto-Rewritten (Cross-Dialect Porting Breaks)
Symptom: PG’s SELECT "userId" FROM "users" becomes SELECT \userId` FROM `users“ (MySQL backticks) after the naive formatter runs. Paste it back to PG and you get a syntax error — PG does not recognize backticks as identifier quotes.
Why: The formatter sees a double quote and rewrites it under its dialect’s “identifier-quote rules” without considering what kind of quote the user originally typed. If the user originally had PG / Standard / SQLite double quotes, rewriting to backticks is a syntax error. If the user originally had MySQL backticks, rewriting to double quotes is also wrong in MySQL (double quotes are strings in MySQL, not identifiers, unless ANSI_QUOTES SQL mode is on).
Fix: The formatter should by default preserve the input quote characters and never actively rewrite them. If the user genuinely wants to rewrite the dialect (e.g., PG → MySQL), that is a separate tool’s job — a schema migration tool (Prisma / SQLAlchemy / Flyway), not the formatter. The Piick SQL Formatter strictly preserves input quote characters — whatever quotes you paste are the quotes in the output, never rewritten.
Real scenario: A data science team exports a query from PG to a BI tool, and the BI tool uses a MySQL-compatible engine. The data scientist manually changes SELECT "userId" to SELECT userId (drop the quotes, since lowercase doesn’t need them). This is human judgment, not the formatter’s job. The formatter breaking already-correct SQL is a most basic bug.
Foot-Gun 6: Non-Recursive WITH (CTE) Formatting (Multiple CTEs Pile Up on One Line)
Symptom: WITH active_users AS (SELECT id FROM users WHERE active=1), recent_orders AS (SELECT user_id FROM orders WHERE created_at > NOW() - INTERVAL '7 days') SELECT COUNT(*) FROM active_users a JOIN recent_orders r ON a.id = r.user_id — after the naive formatter, all CTE names stay on the first line after WITH, the CTE bodies are not indented, and it reads just like unformatted input.
Why: WITH (CTE) is the subquery-naming mechanism introduced in the SQL standard. One WITH can have multiple CTE names (comma-separated), each followed by a (SELECT ...) definition. A naive formatter lays out “WITH … SELECT” as one line and doesn’t even consider that CTE is a list.
Fix: The formatter must recognize the CTE list, give each CTE name its own line, indent the body 1 level (and the SELECT body itself another 1 level). When multiple CTEs nest (CTE body uses WITH again), recurse for each layer. The Piick SQL Formatter recursively formats WITH — each CTE name on its own line + indented body, nested CTEs automatically recursed.
Real scenario: A data team’s analytics query typically chains 5-10 CTEs into a pipeline. The naive formatter flattens it into 3 lines; the reviewer cannot read what each step does. With recursive CTE formatting, each CTE is like an independent mini-function, improving readability 10x.
Foot-Gun 7: All Subqueries Inlined (Derived Tables Lose Their Structure)
Symptom: SELECT u.name, o.total FROM users u JOIN (SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id) o ON u.id = o.user_id — the naive formatter inlines (SELECT user_id, ... GROUP BY user_id) on the JOIN line, and the right pane cannot show a single 80-character line.
Why: Subqueries have two semantics — scalar subquery (as part of an expression, e.g., WHERE id IN (SELECT ...)) should be inlined because it is itself a value; derived table (in the FROM clause, FROM (SELECT ...) AS t) should be on its own line because it is a table, and only an independent line can show its structure clearly. The naive formatter doesn’t distinguish and inlines everything.
Fix: The formatter must distinguish scalar subquery vs derived table — derived tables appearing in the FROM clause go on their own line + indented; scalar subqueries stay inline. The Piick SQL Formatter follows this rule — derived tables automatically get their own line, scalars stay inline.
Real scenario: In a complex BI query, the FROM clause has 3-4 derived tables (each a materialized version of a CTE); after inlining, the entire SQL crowds past 80 characters and you have to manually scroll to see the rest. After proper formatting, each derived table looks like a mini-table, and readability jumps instantly.
Foot-Gun 8: Minified SQL Loses Readability (Debugging Cannot See Structure)
Symptom: Production SQL is ORM-generated and runs fine after minification. But when you pull it from the slow-query log, you get a 5KB single-line SQL and cannot grep WHERE because the minified SQL has no newlines and reads as 5000 characters on one line to the human eye.
Why: Minification’s purpose is to reduce network transmission bytes + let the database parser parse faster (although this benefit is negligible on modern DBs), not for human reading. Developers use minified SQL in production and that’s fine, but for debugging you must see the pretty-printed version to locate issues.
Fix: Production deploys use minified SQL (save transmission); debug uses pretty-printed SQL (readability). This means you need 2 versions — ORM generates minified, the formatter tool generates pretty-printed for debugging. The logging system should also format slow queries before recording, not record raw minified. The Piick SQL Formatter supports Minify mode + 5-metric stats (statement / keyword / identifier / string / byte). Paste the production minified SQL to instantly pretty-print + see statement and keyword counts.
Real scenario: A DBA investigates a slow query; the log gives an ORM-generated minified SQL; the DBA cannot see which JOIN is N+1. Throw this SQL into the Piick SQL Formatter, select Standard SQL + 2-space indent + leading comma, see the structure in 1 second, and locate LEFT JOIN order_items oi ON o.id = oi.order_id WHERE oi.id IS NULL as the N+1 source.
Tool Selection Decisions
Option A: Editor Plugin (SQLTools / DataGrip / DBeaver)
Best for: People who write SQL every day and already use an IDE.
Pros: Real-time formatting, no leaving the editor, automatic integration with schema autocomplete. Cons: Only local SQL, cannot sync across dialects (your IDE has a MySQL formatter, production is PG, quote rewriting breaks), no batch processing (CI auto-format requires a separate setup).
Option B: npm sql-formatter Package
Best for: Auto-formatting in CI / build pipeline, projects with a Node.js environment.
Pros: Can integrate into CI, CI fails directly with an error, version-pinned to ensure team consistency. Cons: ~200KB dependency + a bunch of transitive deps, version upgrades occasionally break old SQL, requires Node environment (pure frontend projects can’t use it), no syntax highlight + click-to-jump error positioning (pure text output).
Option C: Piick SQL Formatter
Best for: Cross-dialect processing (the same SQL switched between 5 dialects to see which is correct), projects that don’t want npm packages (pure frontend), sensitive data (production DDL, PII-containing queries) that should not pass through any third-party server, DBAs / team code review needing a quick structural view.
Pros: Zero dependency (pure frontend token + AST processing), 5 dialects (Standard / MySQL / PostgreSQL / SQLite / T-SQL), 3 keyword case modes, 4 indent modes, leading/trailing comma, syntax highlighting, click-to-jump error positioning, 5-metric stats, 100% local processing. Cons: Cannot integrate into CI (use Option B for that), cannot auto-save (pure frontend tool, must manually copy).
Decision tree:
-
Already writing project-level SQL → Option A (IDE plugin)
-
CI integration + Node environment → Option B (npm package)
-
Cross-dialect debugging + sensitive data + install nothing → Option C (Piick)
5 Real-World Scenarios
Scenario 1: Cross-Dialect Data Migration (PG → MySQL)
PG’s SELECT "userId", COUNT(*) FROM "users" WHERE "createdAt" > NOW() - INTERVAL '7 days' GROUP BY "userId" — you want to run it on MySQL (test environment doesn’t have PG, use MySQL temporarily). Steps:
-
Use the Piick SQL Formatter and select PostgreSQL; first format the PG version (preserving double quotes + PG keywords)
-
Manually adjust schema-related items:
"users" -> users(drop quotes, since lowercase doesn’t need them),INTERVAL '7 days'becomesDATE_SUB(NOW(), INTERVAL 7 DAY) -
Switch back to Piick and select MySQL; reformat the modified SQL (so MySQL’s keyword recognition aligns)
-
Paste into MySQL to execute
Don’t: Use naive reverse operations (formatter auto-rewrites PG double quotes to MySQL backticks), the SQL has syntax errors + field name errors piled up, debug time doubles.
Scenario 2: Code Review Cannot Read JOIN Order
Team PR has SELECT * FROM a LEFT JOIN b ON a.id = b.a_id INNER JOIN c ON b.c_id = c.id WHERE ..., reviewer stares at it for 5 minutes without understanding the JOIN order semantics. Use the Piick SQL Formatter with Standard SQL + 4-space indent; paste it and see the structure in 1 second:
SELECT
FROM
a
LEFT JOIN b
ON a.id = b.a_id
INNER JOIN c
ON b.c_id = c.id
WHERE
…
Reviewer immediately sees “first LEFT JOIN b (may expand rows), then INNER JOIN c (filter out rows where b didn’t match)”, performance issue is clear at a glance.
Scenario 3: Slow-Query Log Minified SQL Restored
DBA gets an ORM-generated 5KB single-line SQL, cannot see the N+1. Paste into the Piick SQL Formatter, switch from Minify to 4-space indent; see the structure in 1 second, locate LEFT JOIN order_items oi ON o.id = oi.order_id WHERE oi.id IS NULL as the N+1 source. Combined with Piick’s Regex Tester to grep keywords in EXPLAIN ANALYZE output, pinpoint the specific slow subquery.
Scenario 4: CTE-Nested Pipeline (Data Team Analytics Query)
Data team’s query chains 8 CTEs into a pipeline; the naive editor flattens it into 80-character one-liners, and the reviewer cannot tell what each step does. Use the Piick SQL Formatter for recursive WITH formatting, each CTE becomes an independent mini-function:
WITH
step1_active_users AS (
SELECT
id
FROM
users
WHERE
active = 1
),
step2_recent_orders AS (
…
),
step3_joined AS (
SELECT
…
FROM
step1_active_users a
JOIN step2_recent_orders r
ON a.id = r.user_id
)
SELECT
…
FROM
step3_joined
Reviewer can read the pipeline like code.
Scenario 5: Production DDL Never Leaves the Browser
Data team needs to sync PG’s CREATE TABLE users (...) to staging; the DDL contains sensitive fields (encrypted user ID field schema). Cannot use an online formatter (afraid SQL gets uploaded); use the Piick SQL Formatter for pure-frontend processing, 100% local, paste, copy the result, close the page, no trace of the SQL. Combined with Piick’s URL Encoder/Decoder to handle URL-encoded field names in DDL, and the JSON Formatter for JSON / JSONB defaults in DDL.
Recommended Practices
-
Always use a formatter with dialect options; at least cover Standard / MySQL / PG / SQLite / T-SQL among the 5 dialects. A naive formatter will inevitably break in cross-dialect projects
-
Always preserve input quote characters (double quote / backtick / bracket), never actively rewrite. Rewriting is the schema migration tool’s job, not the formatter’s
-
Treat comments as first-class citizens; the formatter does not delete comments — comments are the project’s time machine, the only evidence 6 months later explaining “why this was written this way”
-
Recursive processing for WITH (CTE); each CTE gets its own line, indented body, nested CTEs recurse again
-
Distinguish scalar subquery vs derived table: scalars inline (they are values), derived tables on their own line (they are tables)
-
Production uses minified, debug uses pretty-printed; 2 versions each serve their own purpose
-
CI integration: project-level SQL uses the
sql-formatternpm package (or equivalent) for unified formatting, to avoid repeated format changes during review -
Sensitive DDL uses a pure-frontend tool like Piick: 100% local, paste → copy → close page, no trace
-
Stats are essential: after pasting, look at the 5 metrics (statements / keywords / identifiers / strings / bytes) for a quick sanity check on SQL structure (e.g., identifier count suddenly spikes, meaning FROM JOIN added a bunch of tables, possibly ORM generated wrong)
SQL formatting is readability infrastructure for query audits and code review, not decoration. Bookmark the Piick SQL Formatter — for cross-dialect debugging / slow-query log restoration / production DDL investigation, open it and you’re done in 1 minute. Combined with the Piick JSON Formatter for JSON / JSONB columns, the Piick URL Encoder/Decoder for URL fields in WHERE clauses, the Piick Regex Tester for grepping keywords in EXPLAIN output, and the Piick Cron Generator for cron expressions scheduling SQL — these 5 tools together cover the full SQL upstream/downstream scenarios.