As @adamo's answer explains, nextval(..) can be out of sync and no longer incrementing upon the max value. In my case it was due to a logical replication and switch-over, where PostgreSQL leaves all sequence numbers as they were at subscription time.
For larger databases, here are some convenient functions for querying which integer primary keys are out of sync, and to update them.
Verify
CREATE OR REPLACE FUNCTION primary_id_sequences_from_all_tables()
RETURNS TABLE (table_name TEXT, column_name TEXT, data_type TEXT, max BIGINT, next BIGINT) AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT tc.table_name, kcu.column_name, c.data_type
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.columns AS c
ON kcu.table_name = c.table_name
AND kcu.column_name = c.column_name
AND c.data_type IN ('smallint', 'integer', 'bigint', 'decimal', 'numeric', 'real', 'double precision')
WHERE tc.constraint_type = 'PRIMARY KEY'
LOOP
RETURN QUERY EXECUTE 'SELECT ' || quote_nullable(rec.table_name) || ', ' || quote_nullable(rec.column_name) || ', ' || quote_nullable(rec.data_type) || ', (SELECT COALESCE(MAX(' || rec.column_name || '), 0)::BIGINT FROM ' || rec.table_name || '), (SELECT nextval(pg_get_serial_sequence(' || quote_nullable(rec.table_name) || ', ' || quote_nullable(rec.column_name) || ')))';
END LOOP;
END;
$$ LANGUAGE plpgsql;
n.b., the function calls nextval which will have a side-effect of incrementing the sequence values. It shouldn't be a problem, but if you want to avoid this you can switch this to pg_sequence_last_value. However this may only show primary keys for which nextval has already been called.
Call the function to verify the list:
SELECT table_name, column_name, data_type, max, next, (max - next) AS diff
FROM primary_id_sequences_from_all_tables()
WHERE next IS NOT NULL AND max > next
ORDER BY diff DESC;
Update
If you are satisfied and wish at your own risk to fudge the nextval() to be max(..) + 1 for all those columns you can run this function:
CREATE OR REPLACE FUNCTION fixup_primary_id_sequences_in_all_tables()
RETURNS TABLE (table_name TEXT, column_name TEXT, max BIGINT, next BIGINT, set_to BIGINT) AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT seqs.table_name, seqs.column_name, seqs.max, seqs.next
FROM primary_id_sequences_from_all_tables() AS seqs
WHERE seqs.next IS NOT NULL AND seqs.max > seqs.next
LOOP
RETURN QUERY EXECUTE 'SELECT ' || quote_nullable(rec.table_name) || ', ' || quote_nullable(rec.column_name) || ', ' || rec.max || '::BIGINT, ' || rec.next || '::BIGINT, setval(pg_get_serial_sequence(' || quote_nullable(rec.table_name) || ', ' || quote_nullable(rec.column_name) || '), (SELECT MAX(' || rec.column_name || ') FROM ' || rec.table_name || ')+1) AS set_to';
END LOOP;
END;
$$ LANGUAGE plpgsql;
... with this line:
SELECT * FROM fixup_primary_id_sequences_in_all_tables()