Sequence Gaps and Why They're Normal
AI generated
SELECT
JOIN
SQL / Sequences
Sequence Gaps and Why They're Normal
why fighting gaps is usually unnecessary effort

Few behaviors of auto-increment sequences unsettle developers as reliably as an ID column with missing values. The reflex to treat that as a bug and fix it with extra logic almost always causes more problems than it solves: locked tables, blocked transactions, and worse scalability. This article explains why gaps are a structurally unavoidable consequence of rollbacks, sequence caching, and concurrent transaction execution, why that is completely harmless in the vast majority of cases, and where the rare, legitimate exceptions lie.

9 min read Sequence caching Gapless numbers

1. The typical observation: missing IDs in an auto-increment column

A look at a table with an auto-increment primary key occasionally shows an ID sequence like 1001, 1002, 1005, 1006, even though nobody actively deleted any rows. The obvious suspicion is a bug in the application or an error in the database. In reality this is completely normal, well-documented behavior that follows directly from how sequences and transactions work, not from a defect.

The central misconception is assuming a sequence is a transactional resource that gets rolled back along with the transaction. The exact opposite is true, and that difference explains practically every observed gap phenomenon.

2. Why sequences deliberately work outside transactions

A sequence assigns values outside the normal transaction context so multiple concurrent transactions can request new, guaranteed-unique values at the same time without blocking each other. If a sequence value only became final at the commit of the requesting transaction, every other transaction also needing a new value would have to wait until the first transaction commits or rolls back. Under high concurrency, that would turn the sequence into a hard bottleneck.

Because assignment instead happens immediately and independently of the transaction's eventual fate, a once-assigned value stays consumed forever, even if the associated transaction later rolls back. This deliberate design decision trades guaranteed gaplessness for high concurrency, and for the overwhelming majority of use cases that is a very good trade.


BEGIN;
INSERT INTO orders (customer_id) VALUES (42); -- gets e.g. id=1005
ROLLBACK; -- transaction is discarded

BEGIN;
INSERT INTO orders (customer_id) VALUES (43); -- gets id=1006, NOT 1005
COMMIT;

3. Rollbacks as the most common everyday cause of gaps

Every transaction that requests a new sequence value and then rolls back for business or technical reasons, for example because a downstream validation fails, a constraint is violated, or the application throws an exception, leaves a permanent gap behind. In production systems with validation logic, optimistic locking, or external dependencies within a transaction, a certain rate of failing transactions is entirely normal.

The higher the rate of failing or deliberately rolled-back transactions in a system, the more gaps appear. That is not a quality signal about the database, it is a direct, expectable consequence of the volume of transactions that, for good reason, do not go through.

4. Sequence caching as a second, often underestimated cause

Many database systems cache entire blocks of sequence values per database connection or session for performance reasons, instead of assigning a single value per request with corresponding synchronization overhead. If a connection ends or the database restarts before a cached block is fully consumed, the remaining, already reserved values of that block are lost permanently.

This behavior is independent of transaction rollbacks and occurs even in systems with a hundred percent success rate, as soon as connections are regularly re-established, which is the norm in modern applications with connection pooling and autoscaling. Knowing cache size and restart frequency lets you roughly estimate the magnitude of resulting gaps, but not prevent them without giving up the performance benefit of caching.


-- Sequence caching 20 values per session
CREATE SEQUENCE orders_id_seq
    START WITH 1000
    INCREMENT BY 1
    CACHE 20;
-- On a connection drop after only 3 consumed values,
-- the remaining 17 reserved values are lost

5. Concurrent transactions and commit order

Even without a single rollback, concurrent transactions produce another, more subtle phenomenon: the order in which sequence values are assigned does not have to match the order in which the associated transactions actually commit. A transaction that requests a sequence value first can, due to longer processing time, commit later than a second transaction that received a higher value but finished faster.

For application code this means an ID column must never be used as a reliable indicator of the actual chronological order of commits, even when no gap exists at all. For robust chronological ordering, a dedicated timestamp with sufficient resolution is the correct solution, not inference from the ID.

6. Telling a genuine bug apart from a harmless, normal gap

Not every gap is automatically harmless, so it is worth having a way to tell a normal gap apart from an actual bug. A normal gap correlates with a plausible cause: a logged validation failure, a known rollback, a restarted connection pool, or a burst of concurrent writes around the affected ID range. If application logs or transaction logs show a matching event at roughly the same time, the gap is expected behavior, not a defect.

A gap becomes suspicious when it appears without any corresponding rollback, error log entry, or connection event, especially if it recurs in a fixed, implausible pattern. In that case the more likely explanation is an application bug generating and discarding IDs outside the database, for example by pre-fetching a batch of sequence values in the application layer and only using some of them, which reintroduces exactly the caching problem described above under different control.

7. Why fighting gaps is usually unnecessary effort

Trying to avoid gaps, for example by explicitly locking the entire table when assigning a new ID, serializes every write to that one table. What could previously run in parallel becomes a strictly sequential queue, with noticeable effects on throughput and latency as soon as write load rises above a trivial level.

In the overwhelming majority of use cases, a gap in the ID column carries no business meaning whatsoever. The primary key serves as a technical, unique identifier, not as a continuous, gapless count of business events. Once that distinction is made cleanly, the supposed problem disappears without any additional logic being necessary.

8. Rare, legitimate exceptions: when gaplessness is a business requirement

There are cases where gaplessness is not a technical preference but a business or regulatory requirement. The most prominent example is invoice numbers, for which many jurisdictions legally mandate continuous, gapless numbering without jumps, to make manipulation of the books demonstrably impossible. Similar requirements sometimes apply to certain contract numbers or official document numbers.

In these cases the answer is not to manipulate the primary auto-increment sequence or disable its caching, but to treat the gapless number as a separate, business-level attribute managed independently of the technical primary key.

9. How to solve gapless numbers cleanly, separate from the primary key

The robust solution is a dedicated counter table with exactly one row per business context, whose value is read and incremented via SELECT ... FOR UPDATE within the same transaction that creates the invoice. Because this increment happens inside the transaction, it gets rolled back along with it too, unlike with a real sequence, which is what keeps gaplessness guaranteed.

The deliberate downside of this solution is strict serialization of every operation needing a new number for the same context, because the row stays locked until commit. That is almost always acceptable for invoice numbers with comparatively low volume per unit of time, a consciously accepted, clearly bounded trade-off, very different from blanket locking of a heavily used main table.


CREATE TABLE invoice_number_counter (
    context TEXT PRIMARY KEY,
    next_value BIGINT NOT NULL
);

BEGIN;
SELECT next_value FROM invoice_number_counter
    WHERE context = 'DE-2026' FOR UPDATE;
UPDATE invoice_number_counter
    SET next_value = next_value + 1
    WHERE context = 'DE-2026';
-- create the invoice with the read value within the same transaction
COMMIT;
Cause Gap arises from Avoidable without extra cost Business relevant
Transaction rollback Sequence value stays consumed, row disappears No Usually not
Sequence caching Connection drop with an unused block No, only by giving up caching Usually not
Concurrent transactions Commit order diverges from ID order No, structurally inherent Only if misused as a timestamp
Legal invoice numbering Business requirement for gaplessness Yes, via a separate counter table Yes, regulatory binding

Mironsoft

Database optimization, query tuning, and migrations

SQL queries that keep getting slower as the data grows?

We analyze and optimize SQL databases regardless of the system in use, plan safe migrations and schema changes, and teach teams query optimization hands-on.

Query Optimization

Analyze slow queries and speed them up with purpose using indexes and explain plans.

Migration Planning

Execute schema changes and data migrations safely, without downtime.

Team Training

Anchor SQL fundamentals and performance thinking hands-on in the dev team.

10. Summary

Understanding Sequence Gaps

Root cause

Sequences deliberately assign values outside the transaction context so concurrent transactions do not block each other. A rollback therefore never reclaims an assigned value.

Further causes

Sequence caching combined with connection drops, and value assignment independent of commit order, produce additional, equally normal gaps.

Why not fight it

Enforcing gaplessness serializes writes and costs throughput, while a gap in the ID column carries no business meaning in most cases.

Legitimate exception

Legally mandated gapless numbers like invoice numbers belong in a separate, transactionally locked counter table instead of the technical primary key.

11. FAQ: Understanding Sequence Gaps

1Why do gaps appear in an auto-increment column even though nobody deletes rows?
Because sequences deliberately operate outside the transaction context. A once-assigned value stays consumed even if the associated transaction later rolls back.
2Why are sequences designed to be non-transactional?
So multiple concurrent transactions can request new values at the same time without waiting on each other. Transactional assignment would turn the sequence into a hard bottleneck under load.
3Can sequence caching create gaps even without any rollbacks?
Yes, if a database connection ends or the database restarts before a cached block of sequence values is fully consumed, the remaining values of that block are lost permanently.
4Can you infer commit order from the order of sequence values?
No, under concurrent transactions, a transaction with a lower sequence value can commit later than one with a higher value. A dedicated timestamp is required for chronological ordering.
5Why is explicit locking to avoid gaps usually a bad idea?
Because it serializes every write to the affected table, noticeably worsening throughput and latency as soon as write load rises above a trivial level.
6Does a gap in the ID column normally carry business meaning?
In the vast majority of cases, no. The primary key serves as a technical, unique identifier, not as a continuous count of business events.
7For which use case is gaplessness actually mandatory?
The most common example is invoice numbers, for which many jurisdictions legally require continuous, gapless numbering without jumps.
8How do you cleanly solve gapless invoice numbers?
Through a dedicated counter table with one row per business context, whose value is read and incremented via SELECT FOR UPDATE within the same transaction that creates the invoice.
9What is the downside of the counter table solution for invoice numbers?
It serializes every operation needing a new number for the same context, because the row stays locked until commit. At low volume per unit of time this is usually unproblematic.
10Should the primary auto-increment sequence be manipulated for invoice numbers?
No, the primary sequence should remain untouched for the technical primary key. Gapless, business-relevant numbers belong in a separate attribute with its own management logic.