(Llegué a esta pregunta cuando intenté volver a descubrir un artículo sobre este tema. Ahora que lo he encontrado, lo estoy publicando aquí en caso de que otros busquen una opción alternativa a la respuesta elegida actualmente, haciendo una ventana con row_number())
Tengo este mismo caso de uso. Para cada registro insertado en un proyecto específico en nuestro SaaS, necesitamos un número incremental único que se pueda generar frente a mensajes concurrentes INSERTy que, idealmente, no tenga espacios.
Este artículo describe una buena solución , que resumiré aquí por facilidad y posteridad.
- Tenga una tabla separada que actúe como el contador para proporcionar el siguiente valor. Tendrá dos columnas,
document_idy counter. counterserá DEFAULT 0Alternativamente, si ya tiene una documententidad que agrupa a todas las versiones, una counterpodrían añadirse allí.
- Agregue un
BEFORE INSERTdisparador a la document_versionstabla que incremente atómicamente el contador ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter) y luego se establezca NEW.versionen ese valor de contador.
Alternativamente, es posible que pueda usar un CTE para hacer esto en la capa de aplicación (aunque prefiero que sea un desencadenante por razones de coherencia):
WITH version AS (
UPDATE document_revision_counters
SET counter = counter + 1
WHERE document_id = 1
RETURNING counter
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 1, version.counter, 'some other data'
FROM "version";
Esto es similar en principio a cómo intentaba resolverlo inicialmente, excepto que al modificar una fila de contador en una sola declaración, bloquea las lecturas del valor obsoleto hasta que INSERTse confirma.
Aquí hay una transcripción que psqlmuestra esto en acción:
scratch=# CREATE TABLE document_revisions (document_id integer, rev integer, other_data text, PRIMARY KEY (document_id, rev));
CREATE TABLE
scratch=# CREATE TABLE document_revision_counters (document_id integer PRIMARY KEY, counter integer DEFAULT 0);
CREATE TABLE
scratch=# WITH version AS (
INSERT INTO document_revision_counters (document_id) VALUES (2)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter;
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 2, version.counter, 'doc 1 v1'
FROM "version";
INSERT 0 1
scratch=# WITH version AS (
INSERT INTO document_revision_counters (document_id) VALUES (2)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter;
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 2, version.counter, 'doc 1 v2'
FROM "version";
INSERT 0 1
scratch=# WITH version AS (
INSERT INTO document_revision_counters (document_id) VALUES (2)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter;
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 2, version.counter, 'doc 2 v1'
FROM "version";
INSERT 0 1
scratch=# SELECT * FROM document_revisions;
document_id | rev | other_data
-------------+-----+------------
2 | 1 | doc 1 v1
2 | 2 | doc 1 v2
2 | 1 | doc 2 v1
(3 rows)
Como puede ver, debe tener cuidado con cómo INSERTsucede, de ahí la versión de activación, que se ve así:
CREATE OR REPLACE FUNCTION set_doc_revision()
RETURNS TRIGGER AS $$ BEGIN
WITH version AS (
INSERT INTO document_revision_counters (document_id, counter) VALUES (NEW.document_id, 1)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter
)
SELECT INTO NEW.rev counter FROM version; RETURN NEW; END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER set_doc_revision BEFORE INSERT ON document_revisions
FOR EACH ROW EXECUTE PROCEDURE set_doc_revision();
Eso hace que sea INSERTmucho más sencillo y la integridad de los datos más robusta frente a INSERTlos que se originan en fuentes arbitrarias:
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'baz');
INSERT 0 1
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'foo');
INSERT 0 1
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'bar');
INSERT 0 1
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (42, 'meaning of life');
INSERT 0 1
scratch=# SELECT * FROM document_revisions;
document_id | rev | other_data
-------------+-----+-----------------
1 | 1 | baz
1 | 2 | foo
1 | 3 | bar
42 | 1 | meaning of life
(4 rows)