It is absolutely normal to have "holes" in table identity sequences - even if you have never deleted a row. I wrote more about this in this earlier answer.
PostgreSQL sequences are, by design, exempt from normal transactional rules. If they were not then only one transaction at a time could acquire an ID, so you'd never be able to have more than one session inserting into a table at a time. This would lead to miserable performance.
This is explained in the PostgreSQL manual on the nextval
call - the call that get values from sequences:
Important: To avoid blocking concurrent transactions that obtain
numbers from the same sequence, a nextval operation is never rolled
back; that is, once a value has been fetched it is considered used,
even if the transaction that did the nextval later aborts. This means
that aborted transactions might leave unused "holes" in the sequence
of assigned values.
In theory PostgreSQL could maintain a list of deleted, abandoned and unused IDs, but in practice this is prohibitively expensive in performance terms - and extremely hard to implement. Once an application gets an id
with nextval
it's free to use it at any time in the future and some applications use exactly that approach, caching blocks of IDs for better internal concurrency.
Consider generated IDs to be a unique row number - and that's all. It doesn't tell you how many rows there are. It doesn't tell you if one row was inserted after another row (you can use a created_time
timestamp, possibly maintained by a trigger, for that). It doesn't tell you if one row was committed after another row (the system xmin
columm tells you that, with certain limitations). All it tells you is how to find the row.
See: