PostgreSQL Formatter
This PostgreSQL formatter indents a query, sets the keyword case and puts every clause on its own line, inside your browser. Minify collapses it back onto one line.
Loading the formatter.
The result follows what you type. Ctrl+Enter, Cmd+Enter on a Mac, runs it at once. The query stays in this tab.
What it does and how to use it
The box above opens with a query that carries most of what people come here to check, a CTE, DISTINCT ON, a window function, a $1 placeholder, two jsonb operators and an ON CONFLICT ... DO UPDATE. Edit it in place or paste over it and the result pane follows a moment after you stop typing. Ctrl+Enter, or Cmd+Enter on a Mac, will format PostgreSQL query text at once instead of waiting. The Keywords and Indent controls underneath re-run the formatter on whatever is already there, and the two tabs at the top choose between laying the statement out and collapsing it. When the library cannot read your input, its message lands under the result and your text stays put.
How to format a PostgreSQL query without rewriting it
Indentation earns its keep the moment you read a query beside its plan, because EXPLAIN prints one node per line, and a statement laid out the same way lets you match the sequential scan you are worried about to the table it came from without counting parentheses.
Postgres syntax that trips generic formatters mostly survives, with one quirk worth knowing. In the sample above, DISTINCT ON (o.customer_id) keeps its text, though the line break falls after DISTINCT rather than after the closing parenthesis. Casts stay glued to what they cast, so created_at::date never returns as created_at :: date, and ->>, @> and #>> are spaced as the operators they are.
For all that, nothing here checks whether a statement is valid, so select from where comes back neatly indented. The syntax checker runs the real PostgreSQL grammar and reports where it stopped.
Minify a PostgreSQL query for one line
The Minify tab is a PostgreSQL SQL minifier written for this page, since the formatting library has no such mode. It walks the text once, collapses every run of whitespace to a single space and drops both comment forms, the -- line and the /* */ block, counting depth on the block form because PostgreSQL lets those nest. Anything quoted is copied through byte for byte. A string literal keeps its doubled spaces, a "Quoted Ident" keeps its capitals and its spacing, and a dollar-quoted body keeps its line breaks along with its own comments, which is what a DO block inside a migration needs. A $1 is read as a parameter rather than as the opening of a $tag$.
Here is the run behind those claims, taken from the widget code itself. This input, with two comments, a literal carrying doubled spaces and two placeholders,
-- nightly rollup
SELECT o.id,
'two spaces kept' AS note,
"Quoted Ident" /* block /* nested */ comment */
FROM orders o
WHERE o.created_at >= $1::date -- param stays
AND o.payload ->> 'channel' = $2;
comes back as one line with both comments gone and the literal untouched.
SELECT o.id, 'two spaces kept' AS note, "Quoted Ident" FROM orders o WHERE o.created_at >= $1::date AND o.payload ->> 'channel' = $2;
A DO $$ ... $$ block keeps its shape while the statement after it collapses onto the same line as the closing $$;, so a minified migration with a function body in it still spans several lines, and that is correct rather than a bug. Where a comment was removed a space is left behind, which stops select 1 --note on one line and from t on the next from welding into 1from.
Format a query copied out of a Prisma or Drizzle log
The usual reason to want a postgres sql formatter online rather than a plugin in the editor is a query that never lived in the editor. Prisma logs its SQL with every identifier quoted and schema-qualified, "public"."User"."createdAt" and so on, with $1, $2 and $3 standing where the values went. Drizzle's log looks much the same with shorter names. Both paste in as they are, because the formatter reads $1 as a numbered placeholder rather than as broken syntax, so nothing needs substituting first, and a quoted identifier passes through with its case intact, so "User" stays "User" whatever the keyword setting says.
SELECT
"public"."User"."id",
"public"."User"."email"
FROM
"public"."User"
WHERE
(
"public"."User"."id" = $1
AND "public"."User"."deletedAt" IS NULL
)
ORDER BY
"public"."User"."createdAt" DESC
LIMIT
$2
OFFSET
$3
That is the Prisma shape after Format with keywords in upper case. The parentheses the ORM wrapped around the WHERE open onto lines of their own, which is the one thing worth deleting by hand before the query goes into a bug report. The $1 markers survive the trip, because the panel reads a numbered placeholder as a parameter rather than as text to be filled in, so what comes out is still the statement Prisma sent.
Keyword case: upper, lower, as typed
Keywords is the first control under the panel and has three settings. UPPERCASE writes SELECT, FROM and IS NULL in capitals, lowercase writes them small, and As typed leaves every keyword exactly as it arrived, so a Select stays Select and a mixed-case query stays mixed. Only words the dialect knows as keywords are touched. A column called from_date is an identifier and keeps its case under every setting, a string literal that happens to read 'select from where' is data and is never rewritten, and the quoted "select" is an identifier as well, which is what the double quotes are for.
Nothing in the server cares which you pick. PostgreSQL reads key words without regard to case and folds unquoted identifiers to lower case, so the three settings produce three texts the server reads as the same statement. Upper is the default here because the manual's own examples use it, and because a capitalised keyword is the fastest way to find the clause boundaries in a query someone else wrote.
Frequently asked questions
The dialect running here knows both. WITH, WITH RECURSIVE, OVER (PARTITION BY ... ORDER BY ...), ON CONFLICT ... DO UPDATE and RETURNING are indented as Postgres syntax rather than guessed at.
Text between dollar quotes is a string literal to the formatter, so a DO block comes back with its line breaks, indentation and comments intact. Only the SQL around it is reformatted.
No. The library loads into this page and your query is processed in the tab you are reading. Open the network panel, press Ctrl+Enter, and watch nothing go out.
No, it touches whitespace and the case of keywords, and nothing else. Identifiers, string literals, numbers, casts, operators, comments and placeholders are copied through as they came, and Minify removes comments on top of that. A statement that was wrong stays wrong in the same way, which is what the syntax checker is for.
Yes, and the statements split on the semicolon and come back separated by a blank line, so a file with a CREATE TYPE, an ALTER TABLE and an INSERT reads as three blocks. One quirk shows up in exactly that file. ENUM in CREATE TYPE ... AS ENUM (...) is not on the library's keyword list, so it keeps the case you typed and sits against its parenthesis like a function call.
pgFormatter is a Perl program you install and run on your own machine. A JavaScript library, sql-formatter, does the work here, so the layout differs in places. Neither changes what a query does.
Format leaves them in place, a -- comment staying on its own line, a trailing one at the end of the clause it was written after, and a /* */ block moving onto a line of its own. Minify drops both forms, except inside a dollar-quoted body, where the text is a string literal and everything in it survives.
Other free PostgreSQL tools
- Syntax checkerRuns your statement through the grammar Postgres itself uses and reports the same message the server would, with the character position it stopped at.
- Connection string builderFills in host, port, database and credentials, then writes the same connection out as a URI, a psql command, a JDBC URL and a DATABASE_URL. Passwords get percent-encoded for you.
- String escaperQuotes a value for Postgres the way quote_literal does, and also as an E-string, dollar quoted, as a LIKE pattern and as an identifier. Paste any of those back and it returns the plain text.
Further reading: Writing a Postgres seed script that survives the next migration.