What gets formatted
SQL arrives unreadable in predictable places: Hibernate and ActiveRecord logs, slow-query logs, EXPLAIN output, BI tools that generate 300-character SELECT lists, and migration files nobody has touched since they were written. This page lays out SELECT, INSERT, UPDATE, DELETE, MERGE, DDL and CTEs one clause per line, indents subqueries and CASE expressions, and keeps short parenthesised expressions on one line.
Layout is produced by the open-source sql-formatter library. Before it runs, PasteKit’s own tokenizer checks the input for the mistakes that most often break a query in transit: a string literal with no closing quote, an unclosed /* comment, and unbalanced parentheses. These are reported with the exact line and column rather than as a vague parse failure further down.
The checker is lexical, not a database. It will not tell you that a column does not exist, and an incomplete query such as SELECT id FROM orders WHERE still formats. Treat it as a syntax sanity check, not a substitute for running the query.
Choose the right dialect
Databases disagree about basic lexical rules, so the Dialect option matters more than it looks:
- Backticks quote identifiers in MySQL, MariaDB, BigQuery and Spark, but are not valid in SQL Server, where
[brackets]are used. 'it\'s'is a valid string in MySQL, while PostgreSQL and SQL Server only accept the doubled form'it''s'.$1,?,:nameand@nameplaceholders belong to different drivers and databases.- PostgreSQL and Snowflake allow dollar-quoted bodies (
$$ ... $$); Oracle hasq'[...]'strings.
With Standard SQL selected, anything outside the ANSI core, such as @@version or [dbo].[orders], is reported as “not valid Standard SQL syntax” with a hint to choose another dialect. Each database has its own page with specifics: MySQL, PostgreSQL, SQL Server T-SQL, Oracle PL/SQL, SQLite, BigQuery and Snowflake, among others.
Style options
- Keyword case: UPPER (
SELECT), lower or As written. Data types follow the same setting. - Function case: As written, UPPER (
COUNT(*)) or lower. Kept separate because many teams uppercase keywords but not functions. - Indent style: Standard puts each clause on its own line with the body indented below it. Tabular, left-aligned and Tabular, right-aligned put the clause keyword in a fixed-width column with its content beside it, the “river” style some SQL style guides prefer.
- Commas: Trailing or Leading (
, customer_idat the start of each line). Leading commas make it easy to comment out the last column; sql-formatter itself has no such option, so PasteKit rewrites the output without touching commas inside strings or comments. - AND / OR: put logical operators at the Start of line or End of line.
- Blank lines between statements: 0 to 5 empty lines between queries in a script.
Indent size and line width are shared with other formats; the line width also decides how long a parenthesised expression can be before it is broken across lines. For conventions behind these choices, see the SQL style guide.
Keys and minifying
Ctrl/Cmd+Enter formats and Ctrl/Cmd+Shift+M minifies: comments and line breaks are removed, but strings, quoted identifiers, optimizer hints (/*+ ... */) and MySQL /*! ... */ comments are kept. Ctrl/Cmd+Shift+C copies, and Ctrl/Cmd+K opens the command palette. Queries copied from production logs can contain customer emails and IDs; the formatter runs inside your browser, so they stay with you.
Examples
Revenue report from an ORM log
A typical generated query gets one clause per line, with the WHERE conditions split at AND.
select c.id, c.name, count(distinct o.id) as orders, sum(i.qty * i.unit_price) as revenue from customers c join orders o on o.customer_id = c.id join order_items i on i.order_id = o.id where o.status in ('paid', 'shipped') and o.placed_at >= '2026-07-01' and o.placed_at < '2026-10-01' group by c.id, c.name having sum(i.qty * i.unit_price) > 500 order by revenue desc limit 25;SELECT
c.id,
c.name,
count(DISTINCT o.id) AS orders,
sum(i.qty * i.unit_price) AS revenue
FROM
customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items i ON i.order_id = o.id
WHERE
o.status IN ('paid', 'shipped')
AND o.placed_at >= '2026-07-01'
AND o.placed_at < '2026-10-01'
GROUP BY
c.id,
c.name
HAVING
sum(i.qty * i.unit_price) > 500
ORDER BY
revenue DESC
LIMIT
25;
CTE with leading commas and tabular layout
Each CTE is indented as its own query, column lists start with commas, and functions such as RANK() are uppercased.
with monthly as (select date_trunc('month', placed_at) as month, customer_id, sum(total) as spend from orders where status = 'paid' group by 1, 2), ranked as (select month, customer_id, spend, rank() over (partition by month order by spend desc) as rnk from monthly) select month, customer_id, spend from ranked where rnk <= 3 order by month, rnk;WITH monthly AS (
SELECT date_trunc ('month', placed_at) AS MONTH
, customer_id
, SUM(total) AS spend
FROM orders
WHERE status = 'paid'
GROUP BY 1
, 2
)
, ranked AS (
SELECT MONTH
, customer_id
, spend
, RANK() OVER (
PARTITION BY MONTH
ORDER BY spend DESC
) AS rnk
FROM monthly
)
SELECT MONTH
, customer_id
, spend
FROM ranked
WHERE rnk <= 3
ORDER BY MONTH
, rnk;
Migration script with several statements
Three statements on one line are split apart with two blank lines between them and keywords in lower case.
ALTER TABLE orders ADD COLUMN refunded_at TIMESTAMP NULL; UPDATE orders SET refunded_at = updated_at WHERE status = 'refunded'; CREATE INDEX idx_orders_refunded_at ON orders (refunded_at);alter table orders
add column refunded_at timestamp null;
update orders
set
refunded_at = updated_at
where
status = 'refunded';
create INDEX idx_orders_refunded_at on orders (refunded_at);
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
This string is never closed — the ' has no matching 'Explained | A quote inside a value, such as O’Brien, ended the string early, or the query was cut off mid-literal. | Double the inner quote (‘O’‘Brien’). In MySQL, ' also works if you choose the MySQL dialect. |
This '(' is never closedExplained | A subquery, function call or IN list is missing its closing parenthesis, often after editing a long WHERE clause. | Add the missing ) where the expression ends; the position points at the opening bracket that has no partner. |
Unexpected ')' — there is no matching '(' before itExplained | An extra closing parenthesis, frequently left behind after deleting a function wrapper such as COALESCE(. | Remove the stray ) or restore the opening part of the expression. |
"[id]" is not valid Standard SQL syntax | The query uses syntax from a specific database (here SQL Server brackets) while the Dialect is set to Standard SQL. | Pick the matching Dialect, for example T-SQL (SQL Server); the tokenizer then understands its quoting and variables. |
This /* comment is never closed | A block comment has no */, so everything after it would be ignored by the database too. | Close the comment where it should end. |
Frequently asked questions
Does the SQL formatter change what my query does?
No. Only whitespace, line breaks and the case of keywords and functions change. Strings, quoted identifiers and comments are left as they are, so the formatted query runs exactly like the original.
How do I format SQL in SQL Server Management Studio or DBeaver?
DBeaver has Format > Format SQL (Ctrl+Shift+F). SSMS has no built-in formatter, so people use extensions or paste into a tool like this one with the T-SQL dialect selected.
Can it validate my SQL?
It catches lexical problems (unclosed strings and comments, unbalanced brackets) and syntax the chosen dialect does not recognise. It does not know your schema, so a misspelt table or column passes; run EXPLAIN against a test database for that.
Is my query sent to a server?
No. Formatting runs in your browser, so production queries with real customer values never leave your machine.
Should SQL keywords be uppercase?
Databases do not care. Uppercase keywords remain the most common convention because they separate keywords from identifiers at a glance, but lowercase is popular in dbt and analytics code; pick one and apply it consistently.