← All cheatsheets
Data Engineer · #079 · October 20, 2026 · 2 min read

Upsert in SQL: ON CONFLICT, MERGE and the rerun test

Why a plain INSERT fails the rerun test, the upsert in Postgres with ON CONFLICT, the MERGE for BigQuery and Snowflake, and the three ways an upsert still duplicates rows: one page.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

The write every pipeline needs is the one you can run twice. Jobs fail, get retried, get backfilled. If a rerun doubles your rows, the pipeline is not safe. This page covers why a plain INSERT fails that test, the upsert in Postgres, the MERGE everywhere else, and the key without which none of it works. The print-ready A4 PDF is at the bottom.

The problem

  • INSERT run twice gives duplicates.
  • DELETE plus INSERT leaves a hole in the table while it runs.

The answer

  • INSERT ... ON CONFLICT in Postgres, with DO UPDATE SET.
  • MERGE INTO in BigQuery, Snowflake and SQL Server.
  • A unique key: no key, no upsert.

Two syntaxes, one idea

Match on the key. Matched rows update, the rest insert.

PostgreSQL:

INSERT INTO daily (day, total)
SELECT day, SUM(amount)
  FROM staging GROUP BY day
ON CONFLICT (day) DO UPDATE
  SET total = EXCLUDED.total,
      updated_at = now();

BigQuery and Snowflake:

MERGE INTO daily d
USING staging s ON d.day = s.day
WHEN MATCHED THEN
  UPDATE SET total = s.total
WHEN NOT MATCHED THEN
  INSERT (day, total)
  VALUES (s.day, s.total);

In ON CONFLICT, EXCLUDED is the row that was rejected: the new values you tried to insert.

Remember

  • WHEN NOT MATCHED: insert.
  • WHEN MATCHED: update, or delete.
  • updated_at: set it in the update branch.

The trap: the upsert that duplicates anyway

Three ways an upsert still fails the rerun test.

You haveWhat goes wrongThe fix
no unique keyON CONFLICT needs a unique index on the conflict columns; no index, no conflict, plain insertCreate the unique index
duplicate sourceTwo staging rows with the same key in one MERGE is an error on most enginesDedupe staging first
wrong keyMatching on day when the grain is day and store inserts one row per rerunThe key is the grain

The test is simple: run the job twice on the same input and count the rows. Same count, same values, you are done.

The quiz

Which one survives a rerun?

A) INSERT INTO daily (day, total)
   SELECT day, SUM(amount)
   FROM staging GROUP BY day;

B) INSERT INTO daily (day, total)
   SELECT day, SUM(amount)
   FROM staging GROUP BY day
   ON CONFLICT (day) DO UPDATE
   SET total = EXCLUDED.total;

B. Run A twice and every day is in the table twice. B matches on the day: the second run finds the rows and updates them, so the count does not move. It needs a unique index on day.

Frequently asked questions

What is an upsert in SQL?
An upsert writes a row by matching on a key. If a row with that key exists, it is updated. If not, it is inserted. Run it twice on the same input and the table ends up the same, which is what a pipeline needs.
What is the difference between ON CONFLICT and MERGE?
They express the same idea in different engines. INSERT ... ON CONFLICT ... DO UPDATE is the PostgreSQL syntax. MERGE INTO with WHEN MATCHED and WHEN NOT MATCHED is used in BigQuery, Snowflake and SQL Server.
Why does ON CONFLICT need a unique key?
A conflict is detected by a unique index or constraint on the conflict columns. With no unique key there is nothing to conflict with, so the statement is a plain insert and creates duplicates on every rerun.
Why not use DELETE and INSERT instead of an upsert?
Between the delete and the insert there is a moment when the rows are missing, so readers can see a hole in the table while the job runs. An upsert changes the rows in place, with no such gap.

Get the free PDF

One page, print-ready, free to share. No signup needed.

Download the PDF

More cheatsheets