Postgresql – How to keep an unique counter per row with PostgreSQL

lockingpostgresql

I need to keep an unique (per-row) revision number in a document_revisions table, where the revision number is scoped to a document, so it's not unique to the whole table, only to the related document.

I initially came up with something like:

current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);

But there is a race condition!

I'm trying to solve it with pg_advisory_lock, but the documentation is a bit scarce and I don't fully understand it, and I don't want to lock something by mistake.

Is the following acceptable, or am I doing it wrong, or is there a better solution?

SELECT pg_advisory_lock(123);
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(123);

Shouldn't I lock the document row (key1) for a given operation (key2) instead? So that would be the proper solution:

SELECT pg_advisory_lock(id, 1) FROM documents WHERE id = 123;
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(id, 1) FROM documents WHERE id = 123;

Maybe I'm not used to PostgreSQL and a SERIAL can be scoped, or maybe a sequence and nextval() would do the job better?

Best Answer

Assuming you store all revisions of the document in a table, an approach would be to not store the revision number but calculate it based on the number of revisions stored in the table.

It is, essentially, a derived value, not something that you need to store.

A window function can be used to calculate the revision number, something like

row_number() over (partition by document_id order by <change_date>)

and you'll need a column something like change_date to keep track of the order of the revisions.


On the other hand, if you just have revision as a property of the document and it indicates "how many times the document has changed", then I would go for the optimistic locking approach, something like:

update documents
set revision = revision + 1
where document_id = <id> and revision = <old_revision>;

If this updates 0 rows, then there has been intermediate update and you need to inform the user of this.


In general, try to keep your solution as simple as possible. In this case by

  • avoiding the use of explicit locking functions unless absolutely necessary
  • having fewer database objects (no per document sequences) and storing fewer attributes (don't store the revision if it can be calculated)
  • using a single update statement rather than a select followed by an insert or update