The Invoice Numbers With 132 Gaps
A billing system numbers invoices from a sequence: CREATE SEQUENCE seq_inv START WITH 1000 INCREMENT BY 1 CACHE 20; a BEFORE INSERT trigger assigns seq_inv.NEXTVAL whenever INVOICE_NO is null. After one month the sequence reports 4,312 values issued but INVOICES holds 4,180 rows - 132 gaps. During that month the nightly load failed once on ORA-00001 and was rerun, the instance was bounced twice, and a developer ran a transaction that was rolled back during testing.
The auditor requires strictly increasing, gap-free invoice numbers. The team's first fix was:
ALTER SEQUENCE seq_inv START WITH 5000;
The next invoice still received a value near 4,313, not 5,000. On a test copy they rebuilt the table with INVOICE_NO NUMBER GENERATED ALWAYS AS IDENTITY, and the legacy loader that supplies explicit INVOICE_NO values immediately failed with ORA-32795.
1. Account for the 132 gaps. Which of the events in the month actually caused them, and what does NOCACHE change?
Read the worked answer
NEXTVAL is allocated outside transaction control: once a value is drawn it is consumed forever, regardless of what happens to the transaction that drew it. Three events in that month each produce gaps:
- The failed nightly load (ORA-00001). The trigger fired,
NEXTVALwas consumed, then the insert failed on the unique constraint. Rollback does not return the number. Every failed row in that run burned a value - this is usually the largest contributor. - Two instance bounces.
CACHE 20keeps 20 values in memory per instance. On shutdown or crash, unused cached values are discarded and never reissued. That is up to 20 lost per bounce, and again on each restart if the first session aborts. - The rolled-back test.
INSERT+ROLLBACKstill consumedNEXTVAL. Rollback undoes rows, never sequence state.
Also counted: any invoice the trigger numbered that was later deleted leaves a hole in the table.
NOCACHE only removes cause 2. It shrinks the cache-loss window to zero (at a small performance cost from serialising NEXTVAL), but causes 1 and 3 remain, so the auditor's requirement would still be violated. A sequence is designed to be concurrent and non-transactional; gap-freedom is not a property it can offer at any cache setting. Only a mechanism that is part of the same transaction as the insert can avoid gaps from failed or rolled-back work.
2. Why did ALTER SEQUENCE ... START WITH 5000 not change the next value, and what is the correct 19c statement? Then explain what a genuinely gap-free numbering scheme costs.
Read the worked answer
START WITH sets the initial value used when the sequence is created (or restarted). On an existing sequence it edits the stored metadata but does not reset NEXTVAL - the sequence continues from its current high-water mark, which is why the next invoice came out around 4,313. The correct statement in Oracle Database 19c is:
ALTER SEQUENCE seq_inv RESTART START WITH 5000;
RESTART repositions the sequence; it can optionally take a new START WITH. (The alternative, DROP + CREATE, invalidates every object that referenced the sequence and requires re-granting privileges.) Before restarting, be sure no existing INVOICE_NO is at or above the new start, or the next insert fails with ORA-00001.
Cost of gap-free numbering. You must serialise the allocation with the insert, so the number is only consumed if the transaction commits:
UPDATE invoice_ctr SET last_no = last_no + 1 RETURNING last_no INTO :n;
INSERT INTO invoices (invoice_no, ...) VALUES (:n, ...);
COMMIT;
Consequences: the single counter row is locked until commit, so concurrent invoicing degenerates to one-at-a-time throughput; the counter table is a hot block and a contention point; and the promise still breaks if a transaction commits but the invoice is later deleted, or if the application prints the number before commit and the commit then fails. Gap-free numbering is an accounting control that trades concurrency for a guarantee - decide that consciously rather than by setting NOCACHE.
3. What is the practical difference between GENERATED ALWAYS and GENERATED BY DEFAULT, and why is ORA-32795 a symptom of the legacy loader rather than of identity columns in general?
Read the worked answer
Both variants use a sequence behind the scenes; the difference is who is allowed to supply the value.
GENERATED ALWAYS AS IDENTITYrejects any user-supplied value for the column with ORA-32795: insert into generated always identity column not allowed. The only override is the explicitOVERRIDING SYSTEM VALUEclause, which the legacy loader does not use. That is exactly the failure described: the loader suppliesINVOICE_NOvalues copied from the old system, and the column refuses them.GENERATED BY DEFAULT AS IDENTITYaccepts a supplied value and uses the sequence only when the column is omitted. The trap is silent: supplying a value does not advance the internal sequence, so the next generated number can collide with a migrated one and raise ORA-00001 - the same class of failure, but discovered later and harder to trace.
The two honest options for a migration:
- Keep
GENERATED ALWAYSand change the loader to omitINVOICE_NO, remapping external references - no key collisions, but historical invoice numbers change. - Keep the historical numbers, use
BY DEFAULT, and after the data load synchronise the sequence to the high-water mark (ALTER SEQUENCE ... RESTART START WITH <max+1>). This preserves the numbers but means the identity is only a default, not a guarantee.
A third option - leave the trigger and the sequence in place rather than converting to identity - is often the least disruptive, provided the gap discussion above has been had with the auditor.