Sharon Ben-Moshe is the founder of sqlfmt, a browser-based SQL formatter and validator with misspelled-keyword suggestions across PostgreSQL, MySQL, SQLite, SQL Server, and BigQuery.
Multi-row INSERT alignment, UPDATE SET clause style, DELETE WHERE formatting, and ON CONFLICT/MERGE upsert syntax across five SQL dialects, with examples.
Quick answer
Format multi-row INSERT statements with one row per line and aligned VALUES, put each column on its own line in an UPDATE's SET clause, and always give WHERE its own line in UPDATE and DELETE statements so the affected rows are unambiguous at a glance. For upsert logic, use each dialect's native syntax — PostgreSQL and SQLite's ON CONFLICT DO UPDATE, MySQL's ON DUPLICATE KEY UPDATE, or SQL Server and BigQuery's MERGE — formatting the UPDATE branch the same way. DML formatting matters more than SELECT formatting because these statements rewrite production data, and a misread WHERE clause is expensive to reverse.
FAQs
Should UPDATE and DELETE statements always include a WHERE clause?
Yes — omitting WHERE from an UPDATE or DELETE applies the statement to every row in the table, which is rarely the intent. Format WHERE on its own line so its absence is visually obvious during review, and run the equivalent SELECT with the same WHERE clause first to confirm the row count before executing the UPDATE or DELETE.
What's the difference between PostgreSQL's ON CONFLICT and MySQL's ON DUPLICATE KEY UPDATE?
Both are upsert syntax — insert a row, or update it if a conflict occurs — but PostgreSQL's ON CONFLICT (and SQLite's identical implementation) requires you to name a conflict target such as a column or constraint, while MySQL's ON DUPLICATE KEY UPDATE fires automatically on any unique or primary key violation with no target to specify. PostgreSQL also gives you an EXCLUDED pseudo-table for referencing the attempted values; MySQL's equivalent VALUES() function is deprecated in favor of aliasing the inserted row.
Is SQL Server's MERGE statement safe to use in production?
MERGE is functionally supported and widely used, but Microsoft's own documentation and multiple community writeups document real concurrency and correctness bugs, particularly without a HOLDLOCK hint on the target table. Use it for batch, single-threaded operations like nightly upserts, and test thoroughly under concurrent load before relying on it for anything transactional.
Should multi-row INSERT statements use leading or trailing commas?
Either works as long as it matches the convention your team already uses for SELECT column lists — the value of a comma style is consistency, not the direction of the comma. Leading commas make it easier to spot a missing comma when reordering rows, which is why some style guides prefer them for exactly this kind of multi-row VALUES list.
Does formatting change how a DML statement performs?
No — whitespace, indentation, and comma placement have zero effect on the query plan or execution time; the database parses past formatting before it ever reaches the optimizer. Formatting exists entirely for the humans reading and reviewing the statement, which for DML is arguably more important than for SELECT, since a misread DML statement changes data rather than just a result set.
Try it now
Paste your SQL into sqlfmt — format, validate, and catch typos free in your browser.
SQL formatters make SELECT queries readable, but INSERT, UPDATE, and DELETE statements need their own conventions. A misaligned VALUES list or a SET clause crammed onto one line hides exactly what you need to see before running it: which rows change, and what they change to.
This guide covers how to format multi-row INSERT statements, INSERT...SELECT, UPDATE's SET and WHERE clauses, DELETE's WHERE clause, and upsert syntax — ON CONFLICT, ON DUPLICATE KEY UPDATE, and MERGE — across PostgreSQL, MySQL, SQLite, SQL Server, and BigQuery.
Most formatting guides are written SELECT-first, so INSERT, UPDATE, and DELETE formatting gets treated as an afterthought. That's backwards: a formatting mistake in a SELECT gives you an ugly result set, but a formatting mistake in DML changes production data. Every convention below optimizes for one thing — making "which columns, which rows, which new values" answerable at a glance, before you hit execute.
How do you format a multi-row INSERT statement?
Two rules cover most of it: always write out the column list, and give every row its own line. An implicit column order is a landmine — the moment someone adds a column to the table, a bare INSERT INTO orders VALUES (...) silently starts inserting into the wrong slots or errors out. Naming the columns makes the statement self-documenting and immune to schema drift.
With the column list in place, format the VALUES rows the same way you'd format a list of query results: one row per line, consistent indentation, and a trailing or leading comma matching your team's convention elsewhere. Long rows benefit from vertical alignment so an outlier value is easy to spot.
Before: unformatted multi-row INSERT
insert into orders(customer_id,product_id,quantity,unit_price) values (101,55,2,19.99),(102,61,1,44.50),(103,55,5,19.99);
If your team uses leading commas in SELECT lists, apply the same rule to VALUES rows and SET clauses for consistency — see our guide to leading comma style for the reasoning on either convention.
How do you format INSERT ... SELECT?
The same explicit-column-list rule applies, doubled: name the target columns in the INSERT clause and name the source columns in the SELECT, even when SELECT * would technically work. Two column lists side by side let a reviewer confirm the mapping without cross-referencing the table definition.
Format the SELECT portion exactly as you would a standalone query — indent FROM and WHERE, one predicate per line for compound conditions. Nothing about being nested inside an INSERT changes how the SELECT should read.
How should the SET clause in an UPDATE statement be formatted?
One column-value pair per line once you're past two or three assignments. A SET clause with six columns jammed onto a single line forces the reader to scan left to right hunting for the one value that matters; stacked vertically, it reads like a diff.
Put SET and WHERE on their own lines too. Mixing them into a single wrapped line is one of the most common reasons a reviewer misses that an UPDATE has no WHERE clause at all.
How do you format the WHERE clause in UPDATE and DELETE statements?
WHERE is the highest-risk line in any DML statement — it's the difference between changing one row and changing the whole table. Format it the same way you'd format a WHERE clause in a SELECT: one predicate per line, AND and OR left-aligned, parentheses around subqueries indented one level. Our SQL formatting best practices guide covers the general predicate-alignment rules this borrows from.
Subqueries inside a DELETE's WHERE clause deserve the same treatment as a join condition in a SELECT — indented, with the inner SELECT fully formatted rather than squashed onto one line, so a reviewer can verify the subquery's logic independently of the outer statement.
How do you format upsert statements — ON CONFLICT, ON DUPLICATE KEY UPDATE, and MERGE?
Every dialect solves "insert, or update if it already exists" differently, and each syntax has its own UPDATE-shaped clause that needs the same formatting discipline as a plain UPDATE: one assignment per line, clearly separated from the conflict or match condition above it.
PostgreSQL and SQLite: ON CONFLICT DO UPDATE
PostgreSQL's INSERT ... ON CONFLICT DO UPDATE names a conflict target — a column or constraint — and then an UPDATE-style SET clause that can reference the special EXCLUDED pseudo-table for the values that would have been inserted. SQLite adopted the identical ON CONFLICT DO UPDATE syntax, EXCLUDED table included, so the same formatting applies to both. See PostgreSQL's INSERT documentation for the full conflict_target and conflict_action grammar.
MySQL: ON DUPLICATE KEY UPDATE
MySQL's INSERT ... ON DUPLICATE KEY UPDATE has no conflict target — it fires whenever any unique or primary key constraint is violated — so format its UPDATE clause the same way, one assignment per line. The old VALUES() function for referencing the row that failed to insert is deprecated as of MySQL 8.0.20; MySQL's ON DUPLICATE KEY UPDATE documentation recommends aliasing the inserted row and referencing it by name instead.
SQL Server and BigQuery: MERGE
MERGE covers insert, update, and delete in one statement by joining a target table to a source using WHEN MATCHED and WHEN NOT MATCHED clauses. Format each WHEN clause on its own line, indent its UPDATE, INSERT, or DELETE body the same way you would a standalone statement, and always terminate SQL Server's MERGE with a semicolon — it's one of the few statements the engine requires it for, per Microsoft's MERGE (Transact-SQL) reference.
MERGE is also worth a word of caution beyond formatting: Microsoft's own documentation and years of community bug reports acknowledge that MERGE has shipped with real concurrency and correctness issues, including race conditions without a locking hint on the target. Formatting it clearly doesn't fix that — if your MERGE handles anything business-critical, test it under concurrent load before trusting it the way you'd trust separate INSERT and UPDATE statements.
BigQuery's MERGE follows the same ANSI shape — no target-table prefix needed inside the UPDATE SET clause — and is the standard way to do upserts in GoogleSQL, since BigQuery has no ON CONFLICT or ON DUPLICATE KEY UPDATE equivalent, as covered in Google's DML syntax reference for BigQuery.
Every INSERT names its target columns explicitly — no bare VALUES with an implicit column order.
Multi-row INSERT statements have one row per line, aligned and comma-consistent with the rest of the file.
INSERT...SELECT lists source columns explicitly instead of SELECT *.
UPDATE's SET clause has one column-value pair per line once there's more than two or three assignments.
Every UPDATE and DELETE has a WHERE clause on its own line — and you've double-checked it isn't missing.
Subqueries inside WHERE are fully formatted, not squashed onto one line.
Upsert clauses — ON CONFLICT, ON DUPLICATE KEY UPDATE, MERGE — format their UPDATE branch exactly like a plain UPDATE statement.
SQL Server MERGE statements end with a semicolon and have been tested under concurrent load before shipping.
Wrapping up
Multi-row INSERT, UPDATE's SET clause, and every WHERE clause in DML follow the same underlying rule: one meaningful unit per line, aligned consistently, so a human can verify what's about to change before it changes. Upsert syntax varies by dialect, but the same discipline applies to its UPDATE branch regardless of which keyword introduces it.
MERGE INTO inventory AS targetUSING staged_inventory AS source ON target.sku = source.skuWHEN MATCHED THEN UPDATE SET target.quantity = target.quantity + source.quantity, target.updated_at = source.updated_atWHEN NOT MATCHED BY TARGET THEN INSERT (sku, quantity, updated_at) VALUES (source.sku, source.quantity, source.updated_at);
BigQuery: MERGE
MERGE INTO inventory AS targetUSING staged_inventory AS sourceON target.sku = source.skuWHEN MATCHED THEN UPDATE SET quantity = target.quantity + source.quantity, updated_at = source.updated_atWHEN NOT MATCHED THEN INSERT (sku, quantity, updated_at) VALUES (source.sku, source.quantity, source.updated_at);