(Ich bin auf diese Frage gekommen, als ich versucht habe, einen Artikel zu diesem Thema erneut zu entdecken. Nachdem ich ihn gefunden habe, veröffentliche ich ihn hier, falls andere nach einer alternativen Option für die aktuell ausgewählte Antwort suchen - Fensterung mit row_number()
)
Ich habe den gleichen Anwendungsfall. Für jeden Datensatz, der in ein bestimmtes Projekt in unserem SaaS eingefügt wird, benötigen wir eine eindeutige, inkrementelle Nummer, die angesichts gleichzeitiger INSERT
s generiert werden kann und idealerweise lückenlos ist.
Dieser Artikel beschreibt eine schöne Lösung , die ich hier zur Vereinfachung und Nachwelt zusammenfassen werde.
- Haben Sie eine separate Tabelle, die als Zähler dient, um den nächsten Wert bereitzustellen. Es wird zwei Spalten haben,
document_id
und counter
. counter
wird DEFAULT 0
alternativ, wenn Sie bereits eine document
Entität haben, die alle Versionen gruppiert, counter
könnte dort eine hinzugefügt werden.
- Fügen Sie
BEFORE INSERT
der document_versions
Tabelle einen Trigger hinzu, der den Zähler ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
) atomar erhöht und dann NEW.version
auf diesen Zählerwert setzt.
Alternativ können Sie möglicherweise einen CTE verwenden, um dies auf der Anwendungsebene zu tun (obwohl ich es aus Gründen der Konsistenz als Auslöser bevorzuge):
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";
Dies ähnelt im Prinzip dem Versuch, es zu lösen, mit der Ausnahme, dass durch Ändern einer Zählerzeile in einer einzelnen Anweisung Lesevorgänge des veralteten Werts blockiert werden, bis der INSERT
festgeschrieben wird.
Hier ist eine Abschrift davon, psql
wie dies in Aktion gezeigt wird:
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)
Wie Sie sehen können, müssen Sie vorsichtig sein, wie es INSERT
passiert, daher die Trigger-Version, die so aussieht:
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();
Das macht INSERT
s viel einfacher und die Integrität der Daten robuster gegenüber INSERT
s, die aus beliebigen Quellen stammen:
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)