Setting the State of a Postgres Sequence

While migrating data for ipythonblocks.org I ran into an issue that stumped me for about a day: I could read data from my database after the migration, but not insert anything. When I tried, Postgres complained that there I was trying to insert a duplicate primary key. The twist, though, was that I wasn’t specifying a primary key, I expected Postgres to automatically generate and insert a new, valid key. What I ultimately discovered was that because the data I had migrated already contained primary key values, the Postgres sequence created for the table primary key column had not been advanced to the point where it would generate new key values that weren’t already in the table. (A sequence is a stateful number generator kind of like Python’s range.) I had to manually set the sequence state using Postgres’ sequence manipulation functions. This post contains a demonstration of this problem and the solution. SQL was run against Postgres 9.6.3 using pgcli.

First I’ll create a demo table with an integery primary key column (defined as a sequence using Postgres’ serial type) and a text column:

CREATE TABLE demo (
  id BIGSERIAL PRIMARY KEY,
  value TEXT);

Then I’ll insert some data that includes values for the primary key (as when I was migrating data from database to another):

INSERT INTO demo (id, value) VALUES
  (1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E');

As expected, at this point the table has 5 rows:

SELECT * FROM demo;
+------+---------+
| id   | value   |
|------+---------|
| 1    | A       |
| 2    | B       |
| 3    | C       |
| 4    | D       |
| 5    | E       |
+------+---------+

Now I’ll try to insert a new row without specifying a value for the id column. This is how data is most often inserted into a table with a primary key so that the database can generate a new valid key.

INSERT INTO demo (value) VALUES ('F');
duplicate key value violates unique constraint "demo_pkey"
DETAIL:  Key (id)=(1) already exists.

This fails complaining that the key id=1 already exists, and of course it does. But why didn’t Postgres know that and generate a new key of 6? To answer that question I need to examine the sequence backing the primary key of the demo table. We can get a closer look at the table definition using the psql/pgcli \d command:

\d demo;
+----------+--------+----------------------------------------------------+
| Column   | Type   | Modifiers                                          |
|----------+--------+----------------------------------------------------|
| id       | bigint |  not null default nextval('demo_id_seq'::regclass) |
| value    | text   |                                                    |
+----------+--------+----------------------------------------------------+
Indexes:
    "demo_pkey" PRIMARY KEY, btree (id)

The default nextval('demo_id_seq'::regclass) in the Modifiers column means that the default value for the id column is the result of calling the nextval function on the 'demo_id_seq' sequence. nextval is one of Postgres’ sequence manipulation functions, currval is another that allows you to see the value that was returned the last time nextval was called:

SELECT currval('demo_id_seq');
+-----------+
| currval   |
|-----------|
| 1         |
+-----------+

As we saw above when I tried to insert the value 'F' into the table the 'demo_id_seq' sequence tried to give it a key of 1. It turns out that inserting rows into the table that already have values for the primary key column doesn’t advance the sequence that provides values for that column when a default is required. This is something you’ll need to watch out for any time you’re inserting data that includes a value for the primary key column. The solution in my case was to set the current value of the primary key sequence to the maximum value already present using the setval function:

SELECT setval('demo_id_seq', (SELECT max(id) FROM demo));
+----------+
| setval   |
|----------|
| 5        |
+----------+

Using currval again confirms the change:

SELECT currval('demo_id_seq');
+-----------+
| currval   |
|-----------|
| 5         |
+-----------+

nextval('demo_id_seq') should now return 6 and I can retry inserting new data:

INSERT INTO demo (value) VALUES ('F');
SELECT * FROM demo;
+------+---------+
| id   | value   |
|------+---------|
| 1    | A       |
| 2    | B       |
| 3    | C       |
| 4    | D       |
| 5    | E       |
| 6    | F       |
+------+---------+

It worked! And the current value of the sequence is 6 as expected:

SELECT currval('demo_id_seq');
+-----------+
| currval   |
|-----------|
| 6         |
+-----------+

SELECT 'Thanks for reading, you rock!' as signoff;
+-------------------------------+
| signoff                       |
|-------------------------------|
| Thanks for reading, you rock! |
+-------------------------------+
Setting the State of a Postgres Sequence

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.