SQL Formatter
Paste a raw or minified SQL query and get it back with clause keywords, column lists and conditions each on their own line.
About the SQL Formatter
A small tokenizer reads through your query first, so single-quoted string values and double-quoted or backtick-quoted identifiers are treated as one sealed unit and never get split apart even if their contents happen to look like a keyword. Clause words such as SELECT, FROM, WHERE, JOIN, GROUP BY and ORDER BY are matched case-insensitively, uppercased, and each start a fresh line. When a SELECT lists more than one column, they're spread one per line under it, and AND / OR conditions after WHERE, HAVING or ON get their own indented line too, so long conditional chains stay easy to scan.
Why two-word clauses need a dedicated merging pass
SQL has several clause keywords that are actually two separate words functioning as one logical unit, GROUP BY, ORDER BY, INSERT INTO, and every join variant like LEFT JOIN or INNER JOIN. If the formatter treated each word independently, it could easily misread LEFT JOIN as a stray identifier called "LEFT" followed by an unrelated JOIN clause. So after the initial tokenizing pass, a separate merging step specifically looks one token ahead at each point, checks whether the current and next word together match one of these known two-word pairs, and if so, fuses them into a single combined clause token before any line-building logic runs. This is exactly why GROUP BY always stays together on one line rather than splitting into "GROUP" and "BY" on separate lines.
SQL's doubled-quote escaping, and why it's different from JSON or JavaScript
To include a literal quote character inside a quoted string or identifier, SQL's convention is to double it, 'it''s fine' represents the single-quoted string containing "it's fine", rather than using a backslash escape the way JavaScript or JSON strings do. This tokenizer specifically checks for that doubled-quote pattern (src[j] === quote && src[j+1] === quote) and treats it as one embedded literal character rather than mistakenly ending the token early at the first quote it encounters. The exact same doubling convention applies to double-quoted and backtick-quoted identifiers too, which is why both get the identical doubled-character check in the tokenizer.
Why commas inside a function call don't split SELECT columns
When spreading a multi-column SELECT list one column per line, the formatter has to correctly distinguish a comma that separates two columns from a comma that's actually an argument separator inside a function call, like COUNT(a, b) or ROUND(price, 2). It handles this by tracking parenthesis depth as it scans, incrementing on every ( and decrementing on every ), and only treating a comma as a real column boundary when that depth is back at zero. A comma encountered while depth is greater than zero, meaning it's nested inside a function call, gets left alone and stays part of that column's own single line instead of incorrectly splitting a function's arguments across separate output lines.
Why keywords get uppercased but your identifiers never do
Recognized clause and condition words are matched case-insensitively (so select, Select, and SELECT all get recognized identically) but are always written back out in uppercase in the formatted output, a common, widely-followed SQL style convention that visually separates the language's own keywords from your table names, column names, and values. Everything that isn't a recognized keyword, table aliases, column names, string literals, numbers, passes through completely untouched in whatever case you originally typed it, the formatter only ever normalizes the specific words it recognizes as SQL syntax.
How segments decide what starts a new line
Internally, the formatter walks the merged token stream and starts a fresh "segment" (which becomes one line, or one indented group of lines) every time it hits a recognized clause keyword, an AND/OR condition word, or a comment, everything else just keeps accumulating onto whatever segment is currently open. Comments are treated specially, they immediately close out whatever segment was open and get placed on their own line completely untouched, since reflowing text inside a SQL comment could change its meaning or just look wrong once combined with surrounding code.
Frequently Asked Questions
Why does GROUP BY stay together instead of splitting across two lines?
A dedicated merging pass runs after tokenizing that specifically looks for known two-word clause pairs like GROUP BY, ORDER BY, and every join variant, fusing them into one combined token before line-building happens, so they're always treated and displayed as a single logical clause.
How does SQL escape a literal quote character inside a string, and does this tool handle it?
SQL doubles the quote character rather than using a backslash, like 'it''s fine' for a string containing an apostrophe. This tokenizer specifically checks for that doubled-quote pattern and treats it as one embedded literal character, applying the same logic to double-quoted and backtick-quoted identifiers.
Will a function call like COUNT(a, b) in my SELECT list get split incorrectly?
No, the formatter tracks parenthesis depth while splitting SELECT columns, only treating a comma as a real column separator when it's outside any parentheses. A comma inside a function call's argument list stays part of that column's single output line.
Does this formatter change the case of my table and column names?
No, only recognized SQL keywords (SELECT, FROM, WHERE, and similar) get forced to uppercase in the output. Table names, column names, aliases, and string values pass through completely untouched in whatever case you originally typed them.
Are comments preserved exactly as written?
Yes, both line comments (--) and block comments (/* */) are detected during tokenizing and placed on their own line completely untouched, since reformatting text inside a comment could change its meaning or appearance.
Why do AND and OR conditions get their own indented lines?
AND and OR are tracked as a distinct token type from regular clause keywords, and each one starts a fresh indented segment separate from the WHERE, HAVING, or ON clause it belongs to, keeping long chains of conditions easy to scan rather than running together on one dense line.