Transactions and upserts

Make several changes succeed or fail together, and insert-or-update rows in one statement.

Advanced10 min readLesson 38 of 38
In this lesson 7 sections
  1. Transactions: all or nothing
  2. ROLLBACK: the undo button
  3. Upserts: insert or update
  4. Why not DELETE then INSERT?
  5. Common mistakes
  6. Dialect notes
  7. Snowflake notes
Jump to practice

The last lesson in the track covers two tools for changing data safely: transactions, which make a group of changes all-or-nothing, and upserts, which insert new rows and update existing ones in a single statement.

Transactions: all or nothing

Moving a deal between reps might take two statements: update the deal, then log the change. If the first runs and the second fails, the data is half-changed. A transaction prevents that:

BEGIN;                                           -- start: nothing is permanent yet
UPDATE opportunities SET owner_id = 13 WHERE opportunity_id = 1003;
INSERT INTO opportunity_history (opportunity_id, stage, changed_at)
VALUES (1003, 'Qualification', '2026-11-02 09:00:00');
COMMIT;                                          -- make both changes permanent together
Statement What it does
BEGIN Starts a transaction. Changes after this are pending.
COMMIT Makes every pending change permanent, together
ROLLBACK Throws every pending change away, as if they never ran

ROLLBACK: the undo button

BEGIN;
UPDATE accounts SET segment = 'Enterprise';                    -- oops: no WHERE
SELECT COUNT(*) FROM accounts WHERE segment = 'Enterprise';    -- 14: every account
ROLLBACK;
SELECT COUNT(*) FROM accounts WHERE segment = 'Enterprise';    -- 3: back to normal

Lesson 16 said UPDATE without WHERE usually has no undo. Inside a transaction, it does, as long as you ROLLBACK before you COMMIT. A safe habit for risky changes: BEGIN, run the change, check it with a SELECT, then COMMIT if it’s right or ROLLBACK if it isn’t.

If anything fails inside a transaction, roll back. Most tools do it for you when a statement errors.

Upserts: insert or update

Loading data often means “add these rows, but if a row already exists, update it instead”. A plain INSERT fails on the existing rows:

INSERT INTO accounts VALUES (4, 'Birch Dental Group', 'Healthcare', 'Mid-Market', 'South');
-- error: account_id 4 already exists

An upsert (update + insert) handles both in one statement:

INSERT INTO accounts (account_id, name, industry, segment, region)
VALUES (4,  'Birch Dental Group',  'Healthcare', 'Mid-Market', 'South'),
       (15, 'Juniper Vet Clinics', 'Healthcare', 'SMB',        'North')
ON CONFLICT (account_id) DO UPDATE
   SET segment = excluded.segment,
       region  = excluded.region;

Account 15 is new, so it’s inserted. Account 4 already exists, so instead of an error, its segment is updated from SMB to Mid-Market.

Part What it does
ON CONFLICT (account_id) What counts as “already exists”. Must be a primary key or UNIQUE column.
DO UPDATE SET ... What to change on the existing row
excluded.segment The value from the row you tried to insert
DO NOTHING Instead of DO UPDATE: skip rows that already exist

Only the columns you list in SET change. Here the name and industry of account 4 stay as they were.

Why not DELETE then INSERT?

Deleting everything and reloading works, but anything the delete removes is gone if the insert fails halfway, and other tables pointing at those rows can break. An upsert only touches what changed. On big tables, it’s also far less work.

Common mistakes

Mistake Fix
Forgetting COMMIT The changes stay pending, and other users can’t see them. Some tools roll them back when you disconnect.
A long-open transaction It can lock rows other people need. Keep transactions short.
ON CONFLICT on a column with no unique rule The database can’t detect a conflict. It needs a primary key or UNIQUE.
Updating every column in SET Only update what the incoming data should change
An error after BEGIN, then another BEGIN The first transaction is still open. Run ROLLBACK; first, or press Reset in the sandbox.

Dialect notes

Database Upsert Transactions
PostgreSQL INSERT ... ON CONFLICT ... DO UPDATE, as above. MERGE since version 15. BEGIN / COMMIT / ROLLBACK
SQL Server MERGE (see below) BEGIN TRANSACTION / COMMIT / ROLLBACK
MySQL INSERT ... ON DUPLICATE KEY UPDATE segment = VALUES(segment) START TRANSACTION / COMMIT / ROLLBACK
SQLite (the sandbox) ON CONFLICT, as above. Also INSERT OR IGNORE and INSERT OR REPLACE. BEGIN / COMMIT / ROLLBACK

MERGE is the standard, longer form. It matches a source to a target and says what to do in each case. SQLite doesn’t have it, so this one won’t run in the sandbox:

MERGE INTO accounts AS tgt
USING staging_accounts AS src
   ON tgt.account_id = src.account_id
WHEN MATCHED THEN
     UPDATE SET segment = src.segment, region = src.region
WHEN NOT MATCHED THEN
     INSERT (account_id, name, industry, segment, region)
     VALUES (src.account_id, src.name, src.industry, src.segment, src.region);

Snowflake notes

Snowflake uses MERGE for upserts, with the syntax above. It has no ON CONFLICT, partly because it doesn’t enforce primary keys (lesson 37). So make sure the source has one row per key, or MERGE can fail or update a row twice. Transactions use BEGIN / COMMIT / ROLLBACK. Snowflake also has Time Travel: SELECT ... FROM accounts AT(OFFSET => -3600) reads a table as it was an hour ago, and UNDROP TABLE restores a dropped table. In dbt, incremental models generate the MERGE for you.

More in the field guide: MERGE (upsert), Transactions and the MERGE breakdown.

Try it

Load two account records with one upsert. Account 4, Birch Dental Group, already exists and has moved up to Mid-Market, still in the South region. Account 15 is new: Juniper Vet Clinics, Healthcare, SMB, North. Then return account_id, name, segment and region for those two accounts, sorted by account_id.

Write it in the sandbox below, then press Check answer.

SQL sandbox

Run queries against the sample CRM tables (accounts, opportunities,users, contacts and opportunity_history), right in your browser. Nothing you run leaves your computer.

Back to the top of the lesson