PostgreSQL ile satır başına benzersiz bir sayaç nasıl tutulur?


10

Ben revizyon numarası bir belgeye kapsamlandırılmış bir document_revisions tablosunda benzersiz (satır başına) revizyon numarası tutmak gerekir, bu yüzden tüm tablo için benzersiz değil, sadece ilgili belgeye.

Başlangıçta şöyle bir şey buldum:

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

Ama bir yarış durumu var!

Bunu çözmeye çalışıyorum pg_advisory_lock, ancak belgeler biraz az ve tam olarak anlamıyorum ve yanlışlıkla bir şeyi kilitlemek istemiyorum.

Aşağıdakiler kabul edilebilir mi, yoksa yanlış mı yapıyorum yoksa daha iyi bir çözüm var mı?

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

Bunun yerine belirli bir işlem (anahtar2) için belge satırını (anahtar1) kilitlememeli miyim? Bu doğru çözüm olacaktır:

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;

Belki PostgreSQL için alışkın değilim ve bir SERIAL kapsam dahilinde olabilir, ya da bir dizi olabilir ve nextval()işi daha iyi yapar mıydı?


"Belirli bir operasyon için" ile ne demek istediğinizi ve "key2" nin nereden geldiğini anlamıyorum.
Trygve Laugstøl

2
Kötümser kilitleme istiyorsanız kilitleme stratejiniz TAMAM görünüyor, ancak pg_advisory_xact_lock kullanacağım, böylece tüm kilitler otomatik olarak COMMIT / ROLLBACK üzerinde serbest bırakılır.
Trygve Laugstøl

Yanıtlar:


2

Bir tablodaki belgenin tüm revizyonları saklamak varsayarsak, bir yaklaşım olacaktır değil revizyon numarasını saklamak ancak tabloda saklanan düzeltmeler sayısına dayalı olarak hesaplamak.

Aslında, depolamanız gereken bir şey değil, türetilmiş bir değerdir.

Revizyon numarasını hesaplamak için bir pencere işlevi kullanılabilir.

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

ve change_daterevizyonların sırasını takip etmek gibi bir sütuna ihtiyacınız olacak .


Öte yandan, sadece revisionbelgenin bir özelliği olarak var ve "belgenin kaç kez değiştiğini" gösterir, o zaman iyimser kilitleme yaklaşımı için gitmek istiyorum, şöyle bir şey:

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

Bu 0 satırı güncellerse, ara güncelleme yapıldı ve kullanıcıyı bu konuda bilgilendirmeniz gerekir.


Genel olarak, çözümünüzü olabildiğince basit tutmaya çalışın. Bu durumda

  • kesinlikle gerekli olmadıkça açık kilitleme fonksiyonlarının kullanımından kaçınmak
  • daha az veritabanı nesnesine sahip olmak (belge dizisi başına yok) ve daha az nitelik depolamak (düzeltilebiliyorsa düzeltmeyi saklamayın)
  • Tek kullanarak updatebir yerine deyimi selectbir takip insertyaupdate

Gerçekten de, hesaplanabildiği zaman değeri saklamak zorunda değilim. Bana hatırlattığın için teşekkürler!
Julien Portalier

2
Aslında, benim bağlamımda, eski revizyonlar bir noktada silinecek, bu yüzden hesaplayamıyorum ya da revizyon numarası azalacak :)
Julien Portalier

3

SEQUENCE'ın benzersiz olduğu garanti edilir ve doküman sayınız çok yüksek değilse kullanım durumunuz uygulanabilir görünür (aksi takdirde yönetilecek çok fazla diziniz vardır). Dizi tarafından oluşturulan değeri almak için RETURNING deyimini kullanın. Örneğin, belge_kimliği olarak 'A36' kullanmak:

  • Belge başına, artışı izlemek için bir sıra oluşturabilirsiniz.
  • Dizileri yönetmek biraz dikkatle ele alınmalıdır. Tabloyu document_ideklerken / güncellerken başvurmak için belge adlarını ve bununla ilişkili diziyi içeren ayrı bir tablo tutabilirsiniz document_revisions.

     CREATE SEQUENCE d_r_document_a36_seq;
    
     INSERT INTO document_revisions (document_id, rev)
     VALUES ('A36',nextval('d_r_document_a36_seq')) RETURNING rev;

Biçimlendirme deszo için teşekkürler, yorumlarıma yapıştırdığımda ne kadar kötü göründüğünü fark etmedim.
bma

Sonraki değerin işlem içinde çalışmadığı için bir önceki değerin + 1 olmasını istiyorsanız, dizi kötü bir sayaçtır.
Trygve Laugstøl

1
Eh? Diziler atomiktir. Bu yüzden belge başına bir dizi önerdim. Ayrıca, geri dönüşler arttıktan sonra sekansı azaltmadığı için boşluk bırakmaları garanti edilmez. Doğru kilitlemenin iyi bir çözüm olmadığını söylemiyorum, sadece diziler bir alternatif sunuyor.
bma

1
Teşekkürler! Düzeltme numarasını saklamam gerekiyorsa diziler kesinlikle gitmenin yoludur.
Julien Portalier

2
Bir dizi esasen bir sıralı bir tablo olduğundan, büyük miktarlarda diziye sahip olmanın performansta büyük bir hit olduğunu unutmayın.
Bununla

2

Bu genellikle iyimser kilitleme ile çözülür:

SELECT version, x FROM foo;

version | foo
    123 | ..

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

Güncelleme güncellenen 0 satır döndürürse, başka bir satır zaten güncellediği için güncellemenizi kaçırdınız.


Teşekkürler! Belgede bir güncelleme güncellemesi tutmanız gerektiğinde bu iyi olur! Ancak document_revisions tablosundaki her satır için güncellenmeyecek benzersiz bir revizyon numarasına ihtiyacım var ve önceki revizyonun takipçisi olmalı (yani önceki satırın + 1 revizyon numarası).
Julien Portalier

1
Hm, neden bu tekniği kullanmıyorsun? Bu size aralıksız bir sıra verecek tek yöntemdir (kötümser kilitleme dışında).
Trygve Laugstøl

2

(Bu konuyla ilgili bir makaleyi yeniden keşfetmeye çalışırken bu soruya geldim. Şimdi bulduğuma göre, başkalarının şu anda seçili olan cevaba alternatif bir seçenek arayışı içinde olması durumunda buraya gönderiyorum - row_number())

Aynı kullanım senaryo var. SaaS'ımızdaki belirli bir projeye eklenen her kayıt için, eşzamanlı INSERTs karşısında üretilebilen ve ideal olarak boşluksuz benzersiz, artan bir sayıya ihtiyacımız var .

Bu makalede , burada kolaylık ve gelecek için özetleyeceğim güzel bir çözüm açıklanmaktadır .

  1. Sonraki değeri sağlamak için sayaç görevi gören ayrı bir tabloya sahip olun. İki sütunu olacak document_idve counter. counterolacak DEFAULT 0zaten bir varsa, Alternatif documentgrupları tüm sürümleri, bir o varlık counterorada eklenebilir.
  2. Tabloya sayacı ( ) atomik olarak artıran ve daha sonra bu sayaç değerine ayarlanan bir BEFORE INSERTtetikleyici ekleyin .document_versionsUPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counterNEW.version

Alternatif olarak, uygulama katmanında bunu yapmak için bir CTE kullanabilirsiniz (buna rağmen tutarlılık uğruna bir tetikleyici olmasını tercih ederim):

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

Bu, prensipte başlangıçta nasıl çözmeye çalıştığınızla benzerdir, ancak tek bir ifadede bir sayaç satırını değiştirerek, işlenene kadar eski değerin okunmasını engeller INSERT.

İşte psqlbunu eylemden gösteren bir konuşma metni :

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)

Gördüğünüz gibi, nasıl olduğuna dikkat etmelisiniz INSERT, bu nedenle şu şekilde tetikleyici sürümü:

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

Bu, gelişigüzel kaynaklardan kaynaklanan INSERTveriler karşısında verilerin daha bütünlüğünü ve daha sağlam olmasını sağlar INSERT:

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)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.