SQL Formatter
Paste a query and get it laid out: one clause per line, joins and set operators at the margin, boolean operators indented under their clause. It is lexed rather than pattern-matched, so a string literal containing the word from and a comment containing a bracket both survive untouched. Five dialects, each with its own keywords.
Try an example loads a common table expression with a join, a filter and an aggregate, all on one line.
Why a tokenizer, and not a pile of regular expressions
Most browser SQL formatters work by pattern replacement: find the word from, put a newline in front of it, uppercase it. That works on the query in the demo and breaks on real ones, in ways that are worse than not formatting at all because they change what the query means.
select 'from where select' as label,
'a -- not a comment' as note
from t
where t.notes not like '%from where%'
Four hazards in four lines. A string literal containing SQL keywords. A string literal containing a comment marker. A LIKE pattern containing keywords. And in Postgres, dollar-quoted function bodies can contain an entire second query, comments and all.
Here the input is lexed first: strings, comments, quoted identifiers and dollar-quoted blocks are captured whole and copied through byte for byte. Only the tokens outside them are touched. That is what lets the layout be aggressive without ever being dangerous.
Worked example
In, on two long lines:
with recent as (select o.order_id, o.customer_id, sum(l.qty*l.price) as total from orders o join order_lines l on l.order_id = o.order_id where o.placed_at >= date '2026-01-01' and o.status <> 'cancelled' group by 1,2)
select c.region, count(*) as orders, round(avg(r.total),2) as avg_order from recent r join customers c on c.customer_id = r.customer_id where c.region in ('EU','US','APAC') group by c.region having count(*) > 10 order by avg_order desc limit 20;
Out:
WITH recent AS (SELECT o.order_id, o.customer_id, sum(l.qty * l.price) AS total
FROM
orders o
JOIN order_lines l ON l.order_id = o.order_id
WHERE
o.placed_at >= date '2026-01-01'
AND o.status <> 'cancelled'
GROUP BY
1,
2)
SELECT
c.region,
count(*) AS orders,
round(avg(r.total), 2) AS avg_order
FROM
recent r
JOIN customers c ON c.customer_id = r.customer_id
WHERE
c.region IN ('EU', 'US', 'APAC')
GROUP BY
c.region
HAVING
count(*) > 10
ORDER BY
avg_order DESC
LIMIT
20;
Note what did not change: 'cancelled', 'EU' and date '2026-01-01' are all string literals and none of them was uppercased or split. The AND sits indented under its WHERE rather than trailing the previous line, which is what makes a five-condition filter readable. Select-list commas end their lines by default; switch to leading style and they start the next one instead, which some teams prefer because commenting out a line then never breaks the syntax.
Dialect awareness
Small but real. Each dialect brings its own keyword list and its own quoted-identifier characters, so a word that is a keyword in one and a column name in another is treated correctly in both.
| Dialect | Recognizes | Quotes identifiers with |
|---|---|---|
| DuckDB | QUALIFY, EXCLUDE, REPLACE, PIVOT, UNPIVOT, SUMMARIZE, ASOF | " |
| PostgreSQL | RETURNING, ILIKE, LATERAL, MATERIALIZED, CONFLICT | ", plus $tag$ bodies |
| MySQL | STRAIGHT_JOIN, AUTO_INCREMENT, UNSIGNED, DUPLICATE | " and ` |
| SQLite | AUTOINCREMENT, PRAGMA, WITHOUT ROWID, GLOB | ", ` and [] |
| T-SQL | TOP, OUTPUT, MERGE, APPLY, NOLOCK | " and [] |
MySQL also gets backslash escaping inside string literals, which the other four do not have. Getting that wrong means a literal like 'it\'s' is read as ending early, and everything after it is mislaid.
The options
- Keyword case: UPPER, lower, or exactly as you typed it. Upper is the default because it is what most style guides settle on, and because it makes the structure scannable when the identifiers are lowercase.
- Indent: two spaces, four spaces or a tab.
- Comma style: trailing (
a,then a newline) or leading (newline then, b). Leading commas look odd until you have commented out the last column in a select list and not had to fix the comma above it. - Uppercase functions: off by default.
COUNT(*)andcount(*)are both common house styles, and function names are not keywords, so they get their own switch. - Minify strips comments and collapses everything to one line, which is what you want for a query going into a config value or a log line.
Comments survive formatting. A line comment keeps its line, a block comment stays where it was. Only minify removes them, and that is stated on the control.
Where the SQL usually comes from
- An application log. Query logs write everything on one line; this is the fastest way to make one readable.
- A BI tool. Generated SQL is correct and unformatted, often for hundreds of lines.
- An ORM. Same, with more parentheses.
- A colleague's message. Chat clients destroy indentation, and this puts it back.
- This site. The workbench generates DuckDB SQL for every pipeline step you build; paste it here to read it, or take it straight to a database.
Files up to 100 MB can be dropped rather than pasted, which matters for a query log rather than for a single statement.
Frequently Asked Questions
Will it change what my query does?
No. Only whitespace and keyword casing change, and keyword casing is not significant in any SQL dialect. String literals, comments, quoted identifiers and dollar-quoted bodies are captured by the lexer and copied through byte for byte, which is the whole reason it is a lexer and not a set of replacements.
Does it format several statements at once?
Yes. Semicolons end a statement and reset the indent, so a script of ten statements formats as ten blocks. The summary reports how many statements it found.
Which dialect should I pick if mine is not listed?
PostgreSQL is the closest thing to a lingua franca and is a safe default for Redshift, Snowflake, BigQuery standard SQL and most others. The dialect only affects which words are recognized as keywords and which characters quote an identifier, so a wrong choice costs you casing on a few words rather than correctness.
Why are my column names not uppercased?
Because they are not keywords. Only recognized keywords are cased, so SELECT and FROM change while customer_id does not. Function names have their own separate switch, off by default.
Can I get everything back on one line?
Set the mode to Minify. Comments are stripped, whitespace is collapsed, and the result is one line, which is the form you want for a query going into a YAML value, an environment variable or a log.
What happens to a query it cannot parse?
It formats it anyway. The lexer does not need the query to be syntactically valid, only to be lexically sensible, so a half-written query with a missing bracket still comes back laid out. That is deliberate: the moment you most want a formatter is often while you are still fixing the query.
Does anything I paste leave my computer?
No. There is no upload endpoint on this page and no network request in the code that does the work. JavaScript in your own tab reads the text, processes it and hands back the result. Nothing is stored between visits either, so reloading gives you an empty box again. You can confirm it by opening your browser's network panel and watching it stay quiet while you work.
Related
Make the query readable
Free, no account, no upload. A real tokenizer, so your string literals come back exactly as they went in.
Back to the formatter