Bagaimana cara menjaga penghitung unik per baris dengan PostgreSQL?

10

Saya perlu menyimpan nomor revisi unik (per-baris) di tabel document_revisions, di mana nomor revisi dimasukkan ke dokumen, jadi tidak unik untuk seluruh tabel, hanya untuk dokumen terkait.

Saya awalnya datang dengan sesuatu seperti:

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

Tetapi ada kondisi balapan!

Saya mencoba menyelesaikannya pg_advisory_lock, tetapi dokumentasinya agak langka dan saya tidak sepenuhnya memahaminya, dan saya tidak ingin mengunci sesuatu karena kesalahan.

Apakah yang berikut dapat diterima, atau saya salah, atau apakah ada solusi yang lebih baik?

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);

Tidakkah seharusnya saya mengunci baris dokumen (key1) untuk operasi yang diberikan (key2)? Jadi itu akan menjadi solusi yang tepat:

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;

Mungkin saya tidak terbiasa dengan PostgreSQL dan SERIAL dapat dicakup, atau mungkin secara berurutan dan nextval()akan melakukan pekerjaan dengan lebih baik?

Julien Portalier
sumber
Saya tidak mengerti apa yang Anda maksud dengan "untuk operasi yang diberikan" dan dari mana "key2" berasal.
Trygve Laugstøl
2
Strategi penguncian Anda terlihat OK jika Anda ingin penguncian pesimistis, tetapi saya akan menggunakan pg_advisory_xact_lock sehingga semua kunci secara otomatis dirilis pada COMMIT / ROLLBACK.
Trygve Laugstøl

Jawaban:

2

Dengan anggapan Anda menyimpan semua revisi dokumen dalam sebuah tabel, pendekatannya adalah tidak menyimpan nomor revisi tetapi menghitungnya berdasarkan jumlah revisi yang disimpan dalam tabel.

Ini pada dasarnya adalah nilai turunan , bukan sesuatu yang perlu Anda simpan.

Fungsi jendela dapat digunakan untuk menghitung angka revisi, seperti

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

dan Anda akan membutuhkan kolom seperti change_dateuntuk melacak urutan revisi.


Di sisi lain, jika Anda hanya memiliki revisionproperti dokumen dan ini menunjukkan "berapa kali dokumen telah berubah", maka saya akan menggunakan pendekatan penguncian yang optimis, seperti:

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

Jika ini memperbarui 0 baris, maka telah ada pembaruan menengah dan Anda perlu memberi tahu pengguna ini.


Secara umum, cobalah untuk menjaga solusi Anda sesederhana mungkin. Dalam hal ini oleh

  • menghindari penggunaan fungsi penguncian eksplisit kecuali benar-benar diperlukan
  • memiliki lebih sedikit objek database (tidak ada per urutan dokumen) dan menyimpan lebih sedikit atribut (jangan menyimpan revisi jika dapat dihitung)
  • menggunakan updatepernyataan tunggal alih-alih selectdiikuti oleh insertatauupdate
Colin 't Hart
sumber
Memang, saya tidak perlu menyimpan nilai saat itu dapat dihitung. Terima kasih sudah mengingatkan saya!
Julien Portalier
2
Sebenarnya, dalam konteks saya, revisi yang lebih lama akan dihapus di beberapa titik, jadi saya tidak dapat menghitungnya atau jumlah revisi akan berkurang :)
Julien Portalier
3

SEQUENCE dijamin unik, dan case-use Anda terlihat berlaku jika jumlah dokumen Anda tidak terlalu tinggi (kalau tidak, Anda memiliki banyak urutan untuk dikelola). Gunakan klausa RETURNING untuk mendapatkan nilai yang dihasilkan oleh urutan. Misalnya, menggunakan 'A36' sebagai document_id:

  • Per dokumen, Anda bisa membuat urutan untuk melacak kenaikan.
  • Mengelola urutan perlu ditangani dengan hati-hati. Anda mungkin bisa menyimpan tabel terpisah yang berisi nama dokumen dan urutan yang terkait dengan itu document_iduntuk referensi saat memasukkan / memperbarui document_revisionstabel.

     CREATE SEQUENCE d_r_document_a36_seq;
    
     INSERT INTO document_revisions (document_id, rev)
     VALUES ('A36',nextval('d_r_document_a36_seq')) RETURNING rev;
bma
sumber
Terima kasih untuk format deszo, saya tidak menyadari betapa buruknya tampilannya ketika saya menempelkan komentar saya.
bma
Urutan adalah penghitung yang buruk jika Anda ingin nilai berikutnya menjadi +1 sebelumnya karena tidak berjalan dalam transaksi.
Trygve Laugstøl
1
Eh? Urutannya adalah atom. Itu sebabnya saya menyarankan urutan per dokumen. Mereka juga tidak dijamin bebas gap, karena rollback tidak mengurangi urutan setelah bertambah. Saya tidak mengatakan bahwa penguncian yang tepat bukanlah solusi yang baik, hanya saja sekuens tersebut memberikan alternatif.
bma
1
Terima kasih! Urutan pasti cara untuk pergi jika saya perlu menyimpan nomor revisi.
Julien Portalier
2
Perhatikan bahwa memiliki sejumlah besar urutan adalah hit besar pada kinerja, karena urutan pada dasarnya adalah tabel dengan satu baris. Anda dapat membaca lebih lanjut tentang itu di sini
Magnuss
2

Ini sering diselesaikan dengan penguncian optimis:

SELECT version, x FROM foo;

version | foo
    123 | ..

UPDATE foo SET x=?, version=124 WHERE version=123

Jika pembaruan mengembalikan 0 baris yang diperbarui, Anda telah melewatkan pembaruan Anda karena orang lain telah memperbarui baris.

Coba Laugstøl
sumber
Terima kasih! Ini bagus ketika Anda perlu menyimpan konter pembaruan pada dokumen! Tetapi saya memerlukan nomor revisi unik untuk setiap baris dalam tabel document_revisions, yang tidak akan diperbarui, dan harus menjadi pengikut revisi sebelumnya (mis. Nomor revisi dari baris sebelumnya +1).
Julien Portalier
1
Hm, kenapa kamu tidak bisa menggunakan teknik ini? Ini adalah satu-satunya metode (selain dari penguncian pesimistis) yang akan memberi Anda urutan tanpa celah.
Trygve Laugstøl
2

(Saya sampai pada pertanyaan ini ketika mencoba menemukan kembali sebuah artikel tentang topik ini. Sekarang setelah saya menemukannya, saya mempostingnya di sini kalau-kalau ada orang lain yang sedang mengejar opsi alternatif untuk jawaban yang saat ini dipilih — berjendela dengan row_number())

Saya memiliki kasus penggunaan yang sama. Untuk setiap catatan yang dimasukkan ke dalam proyek spesifik dalam SaaS kami, kami membutuhkan angka yang unik dan bertambah yang dapat dihasilkan dalam menghadapi data bersamaan INSERTdan idealnya adalah tanpa celah.

Artikel ini menjelaskan solusi yang bagus , yang akan saya ringkas di sini untuk kemudahan dan keturunan.

  1. Memiliki tabel terpisah yang bertindak sebagai penghitung untuk memberikan nilai berikutnya. Ini akan memiliki dua kolom, document_iddan counter. counterakan menjadi DEFAULT 0Atau, jika Anda sudah memiliki documententitas yang mengelompokkan semua versi, a counterdapat ditambahkan di sana.
  2. Tambahkan BEFORE INSERTpemicu ke document_versionstabel yang secara atom menambah penghitung ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter) dan kemudian setel NEW.versionke nilai penghitung itu.

Atau, Anda mungkin dapat menggunakan CTE untuk melakukan ini di lapisan aplikasi (meskipun saya lebih suka itu menjadi pemicu demi konsistensi):

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";

Ini pada prinsipnya mirip dengan bagaimana Anda mencoba menyelesaikannya pada awalnya, kecuali bahwa dengan memodifikasi baris tandingan dalam satu pernyataan, ia memblokir bacaan nilai basi hingga INSERTdikomit.

Berikut transkrip dari psqlmenunjukkan ini dalam aksi:

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)

Seperti yang Anda lihat, Anda harus berhati-hati tentang bagaimana ini INSERTterjadi, karenanya versi pemicu, yang terlihat seperti ini:

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();

Itu membuat INSERTjauh lebih lurus ke depan dan integritas data lebih kuat dalam menghadapi yang INSERTberasal dari sumber sewenang-wenang:

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)
Bo Jeanes
sumber