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.
Table of Contents
- 1. The typical observation: missing IDs in an auto-increment column
- 2. Why sequences deliberately work outside transactions
- 3. Rollbacks as the most common everyday cause of gaps
- 4. Sequence caching as a second, often underestimated cause
- 5. Concurrent transactions and commit order
- 6. Telling a genuine bug apart from a harmless, normal gap
- 7. Why fighting gaps is usually unnecessary effort
- 8. Rare, legitimate exceptions: when gaplessness is a business requirement
- 9. How to solve gapless numbers cleanly, separate from the primary key
- 10. Summary
- 11. FAQ
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.