Snowflake SQL Formatter

Drop in a Snowflake query or a worksheet full of statements, set Dialect to Snowflake, and format with Ctrl+Enter. Semi-structured paths like `payload:items[0].sku::string` are kept together as one expression.

Input

Settings

History

Load from URL

Snowflake syntax the tokenizer knows about

Before any layout happens, PasteKit splits the text using Snowflake’s own quoting rules. That step is what keeps a formatter from wrecking a query, and Snowflake has a few rules of its own:

  • Single-quoted strings accept both escapes: 'it''s' and 'it\'s' are the same value.
  • Double quotes wrap case-sensitive identifiers, so "Order ID" and "SALES"."PUBLIC"."ORDERS" stay exactly as typed.
  • $$ ... $$ marks a dollar-quoted string. Stored procedure, UDF and EXECUTE IMMEDIATE bodies inside it are kept verbatim: line breaks, casing and inner comments are not touched.
  • // starts a line comment, in addition to -- and /* */.
  • $name session variables, IDENTIFIER($table_name) and :name bind variables are recognised as single tokens.

Semi-structured data, FLATTEN and QUALIFY

Queries over VARIANT columns are where unformatted Snowflake SQL gets hardest to read. Colon paths and casts like e.payload:customer.id::number are left as one unit, and the cast type follows your keyword-case setting (::NUMBER). LATERAL FLATTEN(input => e.payload:items) i lands on its own line in the FROM list, with the => named argument intact.

QUALIFY is placed as a separate clause after WHERE, which suits the common “keep one row per key” pattern. The window spec inside OVER (...) is split into PARTITION BY and ORDER BY lines.

Not every word is upper-cased. Keyword case only applies to words that sql-formatter lists as Snowflake keywords, so over, desc, within and clone keep the case you typed. If you need uniform casing, type those few in the case you want.

Where Snowflake support stops

Some Snowflake-only syntax is outside what sql-formatter can parse, and it is better to know in advance:

  • Stage references such as @raw.orders_stage or @~/staged in FROM or COPY INTO are rejected. In COPY INTO you can put the location in single quotes ('@raw.orders_stage/2026/'), which Snowflake also accepts. For SELECT $1, $2 FROM @stage, swap in a placeholder table name while formatting.
  • ? placeholders are not accepted by the Snowflake grammar here; use :name binds instead.
  • Before a bind variable, the formatter drops the space: status = :status becomes status =:status.
  • CREATE OR REPLACE PROCEDURE headers come out on an awkward line break after CREATE, although the body is preserved.
  • Snowflake Scripting outside $$ (bare DECLARE / BEGIN blocks) is flattened, and INTO :total loses its space. Wrapping the block in EXECUTE IMMEDIATE $$ ... $$ keeps it exactly as written.

Choosing options for Snowflake worksheets

Worksheets often hold a dozen statements in a row. Blank lines between statements controls the gap (0 to 5) after each semicolon. Pick Tabular, left-aligned under Indent style if you like clause keywords padded into a column; Standard is better for deep CTE nesting. Leading commas and AND / OR at the start of the line suit dbt models that change often in review.

Minify is useful for pasting a query into a Python or JavaScript string: whitespace and comments go, while $$ bodies and quoted strings stay unchanged. The SQL minifier page covers that mode in more detail.

Examples

Flatten order line items from JSON events

Each VARIANT path and :: cast stays on one line, and LATERAL FLATTEN joins the FROM list.

Input
select e.event_id, e.payload:customer.id::number as customer_id, i.value:sku::string as sku, i.value:qty::int as qty, i.value:price::number(10,2) as unit_price from raw.order_events e, lateral flatten(input => e.payload:items) i where e.event_type = 'order_placed' and e.loaded_at >= dateadd(day, -1, current_timestamp())
Output
SELECT
  e.event_id,
  e.payload:customer.id::NUMBER AS customer_id,
  i.value:sku::STRING AS sku,
  i.value:qty::INT AS qty,
  i.value:price::NUMBER(10, 2) AS unit_price
FROM
  raw.order_events e,
  LATERAL flatten(input => e.payload:items) i
WHERE
  e.event_type = 'order_placed'
  AND e.loaded_at >= dateadd(day, -1, current_timestamp())
Open this example in the tool

Longest session per user with QUALIFY

The CTE is indented under WITH and QUALIFY gets a clause line of its own, with commas at the start of each line.

Input
with sessions as (select session_id, user_id, min(event_ts) as started_at, max(event_ts) as ended_at, count(*) as events from analytics.web_events where event_date = current_date() - 1 group by session_id, user_id) select user_id, session_id, datediff('second', started_at, ended_at) as duration_s, events from sessions where events > 1 qualify row_number() over (partition by user_id order by events desc) = 1
Output
WITH
  sessions AS (
    SELECT
      session_id
    , user_id
    , min(event_ts) AS started_at
    , max(event_ts) AS ended_at
    , count(*) AS events
    FROM
      analytics.web_events
    WHERE
      event_date = current_date() - 1
    GROUP BY
      session_id
    , user_id
  )
SELECT
  user_id
, session_id
, datediff('second', started_at, ended_at) AS duration_s
, events
FROM
  sessions
WHERE
  events > 1
QUALIFY
  row_number() over (
    PARTITION BY
      user_id
    ORDER BY
      events desc
  ) = 1
Open this example in the tool

COPY INTO with a quoted stage and session variables

Quoting the stage location lets the load statement format, and the // comment stays at the end of its line.

Input
copy into raw.orders from '@raw.orders_stage/2026/' file_format = (type = 'CSV' skip_header = 1 field_optionally_enclosed_by = '"') on_error = 'CONTINUE';
select count(*) from identifier($target_table) where region = $region // session variables
Output
COPY INTO raw.orders
FROM
  '@raw.orders_stage/2026/' file_format = (
    type = 'CSV' skip_header = 1 field_optionally_enclosed_by = '"'
  ) on_error = 'CONTINUE';


SELECT
  count(*)
FROM
  identifier($target_table)
WHERE
  region = $region // session variables
Open this example in the tool

Scripting block kept inside $$

Only EXECUTE IMMEDIATE is upper-cased; everything between the $$ markers is returned exactly as written.

Input
execute immediate $$
declare
  stale_rows number default 0;
begin
  delete from inventory.stock_snapshots where snapshot_date < dateadd(day, -90, current_date());
  stale_rows := sqlrowcount;
  return 'deleted ' || stale_rows;
end;
$$;
Output
EXECUTE IMMEDIATE $$
declare
  stale_rows number default 0;
begin
  delete from inventory.stock_snapshots where snapshot_date < dateadd(day, -90, current_date());
  stale_rows := sqlrowcount;
  return 'deleted ' || stale_rows;
end;
$$;
Open this example in the tool

Common errors and how to fix them

ErrorCauseFix
"@raw.order" is not valid Snowflake syntaxThe query reads from a named stage (@raw.orders_stage), which the formatter cannot parse in a FROM or COPY INTO clause.In COPY INTO, quote the location: FROM ‘@raw.orders_stage/’. In a SELECT over staged files, format with a placeholder table name and put the stage back afterwards.
"?" is not valid Snowflake syntaxA JDBC/ODBC style ? placeholder was left in the query text.Replace each ? with a named bind such as :customer_id, or with a sample literal while you format.
This $$-quoted string is never closed
Explained
A procedure or EXECUTE IMMEDIATE body opened with $$ but the closing $$ was cut off when copying.Add $$ after the final END; of the body, before the terminating semicolon.
This string is never closed — the ' has no matching '
Explained
Snowflake treats a backslash as an escape, so a Windows path like 'C:\exports' escapes its own closing quote.Double every backslash in the literal (‘C:\exports\’) or use a $$ string for text full of backslashes.
Unexpected ')' — there is no matching '(' before it
Explained
An extra closing bracket, usually left behind after deleting a FLATTEN call or an OBJECT_CONSTRUCT argument.Remove the stray ) or restore the call it belonged to.

Frequently asked questions

Does it format Snowflake stored procedures?

It formats the CREATE PROCEDURE statement around the body, but anything between $$ markers is returned unchanged. To tidy the body itself, paste just the SQL statements inside it and format those.

Does it support Snowflake Scripting?

Scripting inside $$ … $$ (for example in EXECUTE IMMEDIATE) is preserved exactly. Bare DECLARE and BEGIN blocks are formatted statement by statement, without block indentation.

Why are some keywords still lowercase after formatting?

Keyword case only applies to words in the Snowflake keyword list that sql-formatter ships. Words outside that list, such as over, desc and clone, keep the case you typed.

Can I format a query copied from Snowsight?

Yes. Paste it, check the Dialect is Snowflake, press Ctrl+Enter, and copy the result back with Ctrl+Shift+C. The query is processed locally in the page and is not uploaded.

Is the output safe to run in dbt?

Formatting changes layout and casing, not the logic of the query. Jinja tags are another matter: {{ ref(‘orders’) }} is reported as “{{” is not valid Snowflake syntax, so format the compiled SQL from the target folder rather than the model template.

Related tools