An identity column collides with imported ids on the next INSERT — why, and how do you reseed it?
answer
- The stored data and the counter are independent
- Explicit values never move the generator
- The primary key is what catches it
- One ALTER sets where the counter resumes
basics
~20 sSupplying explicit values never advances the generator, so after a load it still offers numbers the imported rows already occupy and the primary key rejects them. Fix it with ALTER TABLE ... ALTER COLUMN id RESTART WITH n, above the highest loaded value.
solid answer
~50 sThe generator behind an identity column only moves when it *generates*. Load rows with explicit ids — legal on a `BY DEFAULT` column, or on an `ALWAYS` column via `OVERRIDING SYSTEM VALUE` — and the generator sits exactly where it was. The first ordinary INSERT afterwards offers a low number that an imported row already holds, and the `PRIMARY KEY` rejects it with a duplicate-key error. Note which component actually saved you: the constraint, not the identity declaration. The repair is to reseed the generator as part of the same maintenance step: ```sql ALTER TABLE orders ALTER COLUMN order_id RESTART WITH 100001; ``` Derive the starting point from `SELECT MAX(order_id) + 1 FROM orders`, and run it before traffic resumes — a reseed while concurrent inserts are running is a race, not a fix. Vendor spellings differ (MySQL `ALTER TABLE orders AUTO_INCREMENT = 100001`, SQL Server `DBCC CHECKIDENT`), so script it per engine.
go deeper
Grasp the core fact: supplying your own values does not move the counter, so the counter can later hand out a number that is already taken.
Explain that the primary key constraint, not the identity declaration, is what rejects the duplicate, and write the ALTER TABLE ... RESTART WITH statement that repairs it.
Show the operational discipline: reseed inside the same maintenance step, derive the value while writes are quiesced, leave headroom, verify with a test insert, and never fall back to MAX(id)+1 in application code.
Own the policy that makes this a non-event — ALWAYS by default, imports that must declare their override, reseeding scripted into every data-movement tool, and restores audited for generator position.
## What actually happened An identity column has two independent parts: the values physically stored in the column, and a generator that remembers the last number it handed out. Nothing synchronises them. The generator advances **only when it is asked to generate** — that is, when an INSERT omits the column or writes `DEFAULT`. A migration typically does the opposite. It supplies the ids, because downstream data references them: ```sql -- column declared GENERATED BY DEFAULT AS IDENTITY INSERT INTO orders (order_id, customer_id, total) SELECT legacy_id, customer_id, total FROM staging_orders; -- ids 1 .. 100000 ``` The rows land with ids 1 to 100000. The generator is still at 1. The next application INSERT, which omits `order_id`, is offered 1 — and the primary key rejects it. The errors then continue one per insert as the generator crawls upward through occupied territory, which is why the symptom often looks like an intermittent outage rather than a clean failure. ## The repair Reseed the generator above the highest stored value. The ANSI form alters the column: ```sql SELECT MAX(order_id) FROM orders; -- 100000 ALTER TABLE orders ALTER COLUMN order_id RESTART WITH 100001; ``` `RESTART WITH n` sets the *next* value the generator will produce. Vendor equivalents: - MySQL: `ALTER TABLE orders AUTO_INCREMENT = 100001;` - SQL Server: `DBCC CHECKIDENT ('orders', RESEED, 100000);` — note this sets the **last** used value, so the next one is 100001 The off-by-one differs between these forms, which is exactly the sort of detail worth checking against a real row rather than reasoning about at 2 a.m. ## Do it in the same maintenance step The reseed belongs to the import, not to the incident that follows it. Two reasons. First, `MAX(id) + 1` is only trustworthy while nothing else is inserting; run it against a live table and a concurrent insert can land above your computed value, leaving you to repeat the whole exercise. Second, the window between the load and the reseed is a window in which every ordinary insert fails, so leaving it open is choosing an outage. A safe shape is: quiesce writes or run before cutover → load the rows → compute the maximum → reseed → verify with one throwaway insert-and-rollback → open traffic. Leave headroom above the maximum rather than landing exactly on it; nothing depends on the numbers being contiguous, and a comfortable gap costs nothing. ## Prevention **Declare the column `GENERATED ALWAYS`.** Then an accidental explicit id is a rejected statement at the moment it is written, not a landmine. A genuine import still gets through, but it has to say so: ```sql INSERT INTO orders (order_id, customer_id, total) OVERRIDING SYSTEM VALUE SELECT legacy_id, customer_id, total FROM staging_orders; ``` The visible clause is the point: it marks the statement as one that will leave the generator behind, and a review or a runbook can look for it. **Script the reseed with the load.** Whatever tool performs the migration should end with the `RESTART`. Treat the pair as one operation. **Beware of tooling that hides this.** Backup and restore utilities differ in whether they carry the generator's position with the data; a table restored into a fresh schema can arrive fully populated with a generator at 1. The same applies to environment refreshes that copy tables between databases with plain `INSERT ... SELECT`. Any process that moves rows *with* their keys is a process that must reseed. ## What not to do Do not paper over it in the application by computing `MAX(id) + 1` before each insert. That reintroduces the race the generator exists to eliminate, and it will produce duplicates under concurrency rather than errors — which is strictly worse, since the constraint turns them into failed writes at best and mismatched data at worst. Do not switch the column to `GENERATED BY DEFAULT` and have the application supply ids "to be safe". That is the same race with more code. Do not renumber the imported rows to sit above the generator instead of moving the generator. Their keys are referenced by other tables and, often, by data outside the database entirely; changing a key that other rows point at is a much larger operation than resetting a counter. ## The related check While you are there, verify the two other things an import can leave inconsistent: any sequence objects the schema uses directly (they reseed with `ALTER SEQUENCE seq RESTART WITH n`, and dropping the table does not remove them), and whether the identity column's declared type still has room — a legacy dataset whose keys reach into the billions can exhaust an `INTEGER` column that looked generous on an empty table.
- Why not just have the application compute MAX(id) + 1 before each insert?Because two concurrent sessions read the same maximum and choose the same value. The generator exists precisely to hand out distinct numbers under concurrency. The application version turns a fixable seeding problem into a permanent race, and the constraint then rejects one of the two writers.
- How would you make this failure impossible next time?Declare the column `GENERATED ALWAYS AS IDENTITY` so an accidental explicit id is rejected outright, require a genuine import to write `OVERRIDING SYSTEM VALUE`, and script the `RESTART WITH` into the same migration step. The visible override clause is what a review or runbook can check for.
- What else can leave the generator behind besides a deliberate import?Any process that moves rows with their keys: environment refreshes done with `INSERT ... SELECT`, and restores from backup tools that do not carry the generator's position. A table can arrive fully populated with its generator sitting at 1, so treat reseeding as part of any such copy.
The generator is a ticket dispenser and the table is the waiting room. Walk a hundred people in holding tickets you printed yourself and the dispenser is still on number one — it never watched the door.
saying these in an interview costs you the question
- Assumes inserting explicit ids advances the generator
- Suggests computing MAX(id)+1 in application code
- Renumbers imported rows instead of moving the generator
- Runs the reseed while concurrent inserts continue
- Thinks the identity declaration itself prevents duplicates