Why are there gaps in my Postgres id sequence?

Last edited: 1/16/2025

Most database tables have a primary key that is a made-up number, and that number is usually made by a sequence.

You might be surprised to know that gaps in sequence IDs are a normal aspect of working with relational databases in general. It's a common scenario that can seem puzzling at first, but there are several reasons why these gaps occur, and sometimes, this is typically expected and does not indicate a problem with your database.

It's also important to understand the distinction that sequences guarantee uniqueness, but not consecutiveness, and this should not imply any issues relating to data integrity with your database.

How to check the name of your sequence

If you don't know the name of your sequence, it's often formed based on a standard naming convention: table_name_id_seq, where table_name is the name of your table and id is the name of your serial column.

Common reasons for gaps in sequences

  1. Rollbacks One of the most common reasons for a gap is the rollback of a transaction. If you initiate a transaction that includes an insert operation, the sequence responsible for generating the ID for the new row increments. If, for any reason, the transaction doesn't complete successfully—perhaps due to a constraint violation or a deliberate decision to rollback—the insert operation is undone, but the sequence value used is not returned or reused. The documentation explains that as well:

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 and will not be returned again. This is true even if the surrounding transaction later aborts, or if the calling query ends up not using the value. For example an INSERT with an ON CONFLICT clause will compute the to-be-inserted tuple, including doing any required nextval calls, before detecting any conflict that would cause it to follow the ON CONFLICT rule instead. Such cases will leave unused “holes” in the sequence of assigned values.

  1. Deletions Removing rows from your table also creates gaps in the sequence of primary key values. Although deletions do not affect the sequence directly, they contribute to the non-sequential appearance of IDs in your table.

  2. Manual Sequence Adjustments Another source of gaps can be manual interventions, such as incrementing a sequence number directly or setting it to a new value.

  3. Upserts When an upsert is executed, it can still increase the sequence even if it is set to do nothing on conflicts

Checking for gaps

To check for gaps in the sequence of IDs in a PostgreSQL table, you can use a SQL query that compares the sequence of IDs to a generated series of numbers that spans the same range:


_10
SELECT
_10
s.id AS missing_id
_10
FROM
_10
generate_series((SELECT MIN(id) FROM your_table), (SELECT MAX(id) FROM your_table)) s(id)
_10
LEFT JOIN your_table ON your_table.id = s.id
_10
WHERE
_10
your_table.id IS NULL;

This query should help you pinpoint where gaps exist.

Encountering errors and adjusting sequences

In operations involving sequences, you might encounter an error message such as:

Failed to run sql query: duplicate key value violates unique constraint "{table}_pkey"

This error suggests a discrepancy between the sequence-generated IDs and the actual IDs in the table.

To address this, you can check the current value of your sequence or the highest ID in your table with:


_10
postgres=# SELECT max(id) FROM <table_name>;
_10
_10
postgres=# SELECT nextval('{table}_{column}_seq');

And reset the sequence value to match the highest ID plus one: SELECT setval('{table}_{column}_seq', (SELECT max(id) FROM <table_name>) + 1);

Or, alternatively, adjust the sequence to a specific new value: ALTER SEQUENCE '{table}_{column}_seq' RESTART WITH new_value;

Implementing a gapless ID sequence

If your application requires contiguous IDs and you decide to build a gapless sequence. Think twice about this, as it will serialize all transactions that use that “sequence” which will then deteriorate your data modification performance considerably. However, if your application truly requires gapless IDs, then you can use a Trigger: After a successful insert, use a database trigger to assign an ID based on a custom logic that finds the next available gapless ID.