Contents

Data, SQL and Getting to the Answer

Lesson 57 of 66. Sign in to keep your place.

Module 6 · Changing data safely

INSERT, and the INSERT you can run twice

8 min read

A load that produces the same table whether it runs once or ten timesCOPY into stagingno constraints, sonothing can failDeduplicate thesourcerow_number over thekey, keep number oneINSERT ... ONCONFLICTDO NOTHING, or DOUPDATE from EXCLUDEDRun it againand the table does notchangeON CONFLICT works only because a unique index exists to detect the conflict in; without one thedatabase cannot guess which column made the row a duplicate. Deduplicating the source first is notoptional either: the same key twice in one statement is an error, and a useful one.
ON CONFLICT works only because a unique index exists to detect the conflict in; without one the database cannot guess which column made the row a duplicate. Deduplicating the source first is not optional either: the same key twice in one statement is an error, and a useful one.

Figure

The nightly job that ran twice

A script inserts the day’s payments from the provider’s file. It crashes halfway, somebody re-runs it, and now 3,000 payments exist twice. Nothing in INSERT prevents that; it does what it says. The property you want is that running the job again produces the same table as running it once, which has a name, idempotence, and a specific way to get it.

The shapes of INSERT

SQL
INSERT INTO payments (reference, student_id, amount, paid_on, method)
VALUES ('P-10441', 812, 4500.00, '2026-03-14', 'upi');

INSERT INTO payments (reference, student_id, amount, paid_on, method)
VALUES ('P-10442', 813, 4500.00, '2026-03-14', 'upi'),
       ('P-10443', 814, 3000.00, '2026-03-14', 'cash');

INSERT INTO payments (reference, student_id, amount, paid_on, method)
SELECT reference, student_id, amount, paid_on, method
FROM payments_clean
WHERE loaded_at > '2026-03-14';

Name the columns every time. INSERT INTO payments VALUES (...) with no column list depends on the order columns were created in, and breaks the day someone adds one. The multi-row form is far faster than one statement per row, because each statement is a round trip to the server; the INSERT ... SELECT form moves rows from a staging table into a real one and is how the previous module’s clean view becomes a table. RETURNING id at the end of any of them gives back the generated keys, which is how an application learns the id of the row it just created without a second query.

Making it safe to repeat

The reference is unique, so the second run fails on the first duplicate with a constraint error, and loads nothing. That is safe but unhelpful. ON CONFLICT says what to do instead:

SQL
INSERT INTO payments (reference, student_id, amount, paid_on, method)
SELECT ... FROM payments_clean
ON CONFLICT (reference) DO NOTHING;

Rows whose reference already exists are skipped; the rest go in. Run it ten times and the table is the same as after once. Or, when the new row should replace the old:

SQL
ON CONFLICT (reference) DO UPDATE
  SET amount = EXCLUDED.amount,
      method = EXCLUDED.method;

EXCLUDED is the row that would have been inserted. This is the upsert, insert-or-update in one statement, and it is how a daily feed of customer records is kept current without first checking whether each one exists.

What it needs underneath

ON CONFLICT (reference) works only because there is a unique index on reference. The database detects the conflict by trying to insert into that index. Without one you get there is no unique or exclusion constraint matching the ON CONFLICT specification, and the fix is the constraint, not a different statement. This is also why an upsert on a column that is usually unique but not declared so is not possible: the database will not guess.

SQLite uses the same ON CONFLICT syntax. MySQL says INSERT ... ON DUPLICATE KEY UPDATE amount = VALUES(amount). SQL Server and Oracle, and Postgres from version 15, have MERGE, which can also delete and is more general and more verbose. All of them rest on a unique key.

Two rows in the same statement with the same key

An INSERT ... SELECT ... ON CONFLICT whose source contains the same reference twice fails with ON CONFLICT DO UPDATE command cannot affect row a second time. The source has to be deduplicated first, with the row_number pattern from the previous module. The error is doing you a favour: it has found a duplicate in the file before it found its way in.

Bulk loads

For a million rows, COPY is ten to a hundred times faster than any form of INSERT, because it streams the data in one operation instead of parsing a statement per batch. The pattern for a big idempotent load is COPY into a staging table with no constraints, then one INSERT ... SELECT ... ON CONFLICT from staging into the real table. The constraints are checked once, at the boundary between the two.

The habit

Ask of every job that writes: what happens if this runs twice? If the answer is “duplicates”, the job needs a unique key and an ON CONFLICT. If the answer is “an error”, the job is safe but will page somebody. If the answer is “the same table either way”, the job is finished.

Every lesson here is open. Your progress is recorded once you pass the Building Apps with AI exam.

Check your understanding

A nightly load uses INSERT ... SELECT ... ON CONFLICT (email) DO UPDATE into a customers table and fails with “there is no unique or exclusion constraint matching the ON CONFLICT specification”. Emails are in fact unique in the data. What is the fix?

Pick the one you would defend. Your answer stays private.

Next lesson · 8 minUPDATE and DELETE: rehearse it as a SELECT

Stuck on something?Ask Divya, your AI tutor

How to cite: Addaly (2026) ‘INSERT, and the INSERT you can run twice’, in Data, SQL and Getting to the Answer. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/data-and-sql/insert-and-upsert (Accessed: 13 October 2026).