จะรักษาตัวนับที่ไม่ซ้ำกันต่อแถวด้วย PostgreSQL ได้อย่างไร


10

ฉันต้องการเก็บหมายเลขการแก้ไข (ต่อแถว) ที่ไม่ซ้ำกันในตาราง document_revisions ซึ่งหมายเลขการแก้ไขถูกกำหนดขอบเขตไว้ที่เอกสารดังนั้นจึงไม่ซ้ำกันกับทั้งตารางเฉพาะกับเอกสารที่เกี่ยวข้อง

ตอนแรกฉันคิดเรื่อง:

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

แต่มีสภาพการแข่งขัน!

ฉันพยายามที่จะแก้ปัญหาด้วยpg_advisory_lockแต่เอกสารนั้นค่อนข้างหายากและฉันก็ไม่เข้าใจอย่างเต็มที่และฉันไม่ต้องการล็อคบางอย่างโดยไม่ได้ตั้งใจ

ต่อไปนี้เป็นที่ยอมรับหรือฉันทำผิดหรือมีวิธีแก้ปัญหาที่ดีกว่า

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

ฉันไม่ควรล็อคแถวเอกสาร (key1) สำหรับการดำเนินการที่กำหนด (key2) แทนหรือไม่ ดังนั้นจะเป็นทางออกที่เหมาะสม:

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;

บางทีฉันอาจไม่คุ้นเคยกับ PostgreSQL และสามารถกำหนดขอบเขตของ SERIAL หรืออาจเป็นลำดับและnextval()จะทำงานได้ดีขึ้นหรือไม่


ฉันไม่เข้าใจสิ่งที่คุณหมายถึงด้วย "สำหรับการดำเนินการที่กำหนด" และ "key2" มาจากไหน
Trygve Laugstøl

2
กลยุทธ์การล็อคของคุณดูโอเคถ้าคุณต้องการล็อคในแง่ร้าย แต่ฉันจะใช้ pg_advisory_xact_lock ดังนั้นการล็อคทั้งหมดจะถูกปล่อยโดยอัตโนมัติใน COMMIT / ROLLBACK
Trygve Laugstøl

คำตอบ:


2

สมมติว่าคุณจัดเก็บการแก้ไขทั้งหมดของเอกสารในตารางวิธีการจะไม่เก็บหมายเลขการแก้ไข แต่คำนวณตามจำนวนการแก้ไขที่เก็บไว้ในตาราง

มันคือคุณค่าที่ได้มาไม่ใช่สิ่งที่คุณต้องเก็บ

ฟังก์ชั่นหน้าต่างสามารถใช้ในการคำนวณจำนวนการแก้ไขบางอย่างเช่น

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

และคุณจะต้องมีคอลัมน์ที่ต้องการchange_dateติดตามลำดับการแก้ไข


ในทางตรงกันข้ามถ้าคุณมีrevisionคุณสมบัติเป็นเอกสารและระบุว่า "มีการเปลี่ยนแปลงเอกสารกี่ครั้ง" ดังนั้นฉันจะใช้วิธีการล็อกในแง่ดี

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

หากการอัปเดตนี้ 0 แถวแสดงว่ามีการอัปเดตระดับกลางและคุณจำเป็นต้องแจ้งให้ผู้ใช้ทราบ


โดยทั่วไปแล้วพยายามทำให้โซลูชันของคุณง่ายที่สุดเท่าที่จะทำได้ ในกรณีนี้โดย

  • หลีกเลี่ยงการใช้ฟังก์ชั่นล็อคอย่างชัดเจนเว้นแต่จำเป็นจริงๆ
  • มีวัตถุฐานข้อมูลน้อยลง (ไม่เรียงลำดับเอกสาร) และจัดเก็บแอตทริบิวต์น้อยลง (ไม่เก็บการแก้ไขหากสามารถคำนวณได้)
  • ใช้updateคำสั่งเดียวแทนที่จะselectตามด้วยinsertหรือupdate

แน่นอนฉันไม่จำเป็นต้องเก็บค่าเมื่อสามารถคำนวณได้ ขอบคุณที่เตือนฉัน!
Julien Portalier

2
ที่จริงแล้วในบริบทของฉันการแก้ไขที่เก่ากว่าจะถูกลบในบางจุดดังนั้นฉันไม่สามารถคำนวณได้หรือหมายเลขการแก้ไขจะลดลง :)
Julien Portalier

3

SEQUENCE รับประกันว่าจะไม่ซ้ำกันและลักษณะการใช้งานของคุณจะมีผลบังคับใช้หากจำนวนเอกสารของคุณไม่สูงเกินไป (มิฉะนั้นคุณจะมีลำดับการจัดการมากมาย) ใช้ส่วนคำสั่ง RETURNING เพื่อรับค่าที่สร้างขึ้นตามลำดับ ตัวอย่างเช่นการใช้ 'A36' เป็น document_id:

  • ต่อเอกสารคุณสามารถสร้างลำดับเพื่อติดตามการเพิ่มขึ้น
  • การจัดการลำดับจะต้องจัดการด้วยความระมัดระวัง คุณอาจเก็บตารางแยกต่างหากที่มีชื่อเอกสารและลำดับที่เกี่ยวข้องกับสิ่งนั้นdocument_idเพื่ออ้างอิงเมื่อทำการแทรก / ปรับปรุง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;

ขอบคุณสำหรับการจัดรูปแบบ deszo ฉันไม่ได้สังเกตว่ามันดูไม่ดีเมื่อฉันวางความคิดเห็นของฉัน
bma

ลำดับเป็นตัวนับที่ไม่ถูกต้องหากคุณต้องการให้ค่าถัดไปเป็น +1 ก่อนหน้าเนื่องจากไม่ได้ทำงานภายในธุรกรรม
Trygve Laugstøl

1
ใช่มั้ย? ลำดับเป็นอะตอม นั่นเป็นเหตุผลที่ฉันแนะนำลำดับต่อเอกสาร พวกเขายังไม่รับประกันว่าจะไม่มีช่องว่างเนื่องจากการย้อนกลับไม่เพิ่มลำดับหลังจากที่เพิ่มขึ้น ฉันไม่ได้บอกว่าการล็อคที่เหมาะสมไม่ใช่วิธีแก้ปัญหาที่ดี แต่ลำดับที่นำเสนอทางเลือกเท่านั้น
bma

1
ขอบคุณ! ลำดับเป็นวิธีที่แน่นอนหากฉันต้องการเก็บหมายเลขการแก้ไข
Julien Portalier

2
โปรดทราบว่าการมีลำดับจำนวนมากเป็นสิ่งสำคัญในประสิทธิภาพเนื่องจากลำดับนั้นเป็นตารางที่มีหนึ่งแถว คุณสามารถอ่านเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ที่นี่
Magnuss

2

ปัญหานี้มักแก้ไขได้ด้วยการล็อคในแง่ดี:

SELECT version, x FROM foo;

version | foo
    123 | ..

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

หากการอัปเดตส่งกลับ 0 แถวคุณได้พลาดการอัปเดตเนื่องจากมีคนอื่นอัปเดตแถวอยู่แล้ว


ขอบคุณ! นี่เป็นสิ่งที่ดีเมื่อคุณจำเป็นต้องเก็บการอัปเดตในเอกสาร! แต่ฉันต้องการหมายเลขการแก้ไขที่ไม่ซ้ำกันสำหรับแต่ละแถวในตาราง document_revisions ซึ่งจะไม่ได้รับการอัปเดตและจะต้องเป็นผู้ติดตามของการแก้ไขก่อนหน้า (เช่นหมายเลขการแก้ไขของแถวก่อนหน้า + 1)
Julien Portalier

1
หืมทำไมท่านไม่ใช้เทคนิคนี้ล่ะ? นี่เป็นวิธีการเดียว (นอกเหนือจากการล็อคในแง่ร้าย) ที่จะทำให้คุณมีลำดับช่องว่างน้อยลง
Trygve Laugstøl

2

(ฉันมาถึงคำถามนี้เมื่อพยายามค้นพบบทความเกี่ยวกับหัวข้อนี้อีกครั้งตอนนี้ฉันได้พบแล้วฉันกำลังโพสต์ไว้ที่นี่ในกรณีที่คนอื่นกำลังติดตามตัวเลือกอื่นสำหรับคำตอบที่เลือกในปัจจุบัน - หน้าต่างด้วยrow_number())

ฉันมีกรณีใช้เดียวกันนี้ สำหรับแต่ละระเบียนใส่เข้าไปในโครงการเฉพาะใน SaaS ของเราเราต้องไม่ซ้ำกันจำนวนที่เพิ่มขึ้นซึ่งสามารถสร้างขึ้นในการเผชิญกับความพร้อมกันINSERTและเป็นความนึกคิดที่ไม่มีช่องโหว่

บทความนี้จะอธิบายวิธีแก้ปัญหาที่ดีซึ่งฉันจะสรุปที่นี่เพื่อความสะดวกและลูกหลาน

  1. มีตารางแยกต่างหากซึ่งทำหน้าที่เป็นตัวนับเพื่อให้ค่าถัดไป มันจะมีสองคอลัมน์และdocument_id จะเป็นอีกวิธีหนึ่งถ้าคุณมีเอนทิตีที่จัดกลุ่มเวอร์ชันทั้งหมดแล้วcountercounterDEFAULT 0documentcounterอาจเพิ่มได้
  2. เพิ่มBEFORE INSERTทริกเกอร์ในdocument_versionsตารางที่เพิ่มตัวนับจำนวนอะตอม ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter) จากนั้นตั้งค่าNEW.versionเป็นตัวนับนั้น

อีกวิธีหนึ่งคุณอาจใช้ CTE เพื่อทำสิ่งนี้ได้ในเลเยอร์ของแอปพลิเคชัน (แต่ฉันชอบที่จะเป็นตัวกระตุ้นให้เห็นถึงความมั่นคง):

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

นี่เป็นหลักการที่คล้ายกับวิธีที่คุณพยายามแก้ไขในตอนแรกยกเว้นว่าโดยการปรับเปลี่ยนแถวเคาน์เตอร์ในคำสั่งเดียวมันบล็อกจะอ่านค่าเก่าค้างจนกว่า INSERTจะมีความมุ่งมั่น

นี่คือหลักฐานจากการpsqlแสดงสิ่งนี้ในทางปฏิบัติ:

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)

อย่างที่คุณเห็นคุณต้องระวังเกี่ยวกับการINSERTเกิดขึ้นของมันด้วยเหตุนี้รุ่นทริกเกอร์ซึ่งมีลักษณะดังนี้:

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

สิ่งนี้ทำให้INSERTตรงไปตรงมามากขึ้นและความสมบูรณ์ของข้อมูลที่แข็งแกร่งยิ่งขึ้นเมื่อเผชิญกับ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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.