Làm cách nào để giữ một bộ đếm duy nhất trên mỗi hàng với PostgreSQL?


10

Tôi cần giữ một số sửa đổi (mỗi hàng) duy nhất trong bảng tài liệu.

Ban đầu tôi nghĩ ra một cái gì đó như:

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

Nhưng có một điều kiện cuộc đua!

Tôi đang cố gắng giải quyết nó pg_advisory_lock, nhưng tài liệu này hơi khan hiếm và tôi không hiểu hết về nó và tôi không muốn khóa một cái gì đó do nhầm lẫn.

Là những điều sau đây được chấp nhận, hoặc tôi đang làm sai, hoặc có một giải pháp tốt hơn?

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

Tôi không nên khóa hàng tài liệu (key1) cho một thao tác nhất định (key2) chứ? Vì vậy, đó sẽ là giải pháp thích hợp:

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;

Có lẽ tôi chưa quen với PostgreSQL và SERIAL có thể được đặt trong phạm vi hoặc có thể là một chuỗi và nextval()sẽ thực hiện công việc tốt hơn?


Tôi không hiểu ý của bạn với "cho một hoạt động nhất định" và "key2" đến từ đâu.
Trygve Laugstøl

2
Chiến lược khóa của bạn có vẻ ổn nếu bạn muốn khóa bi quan, nhưng tôi sẽ sử dụng pg_advisory_xact_lock để tất cả các khóa được tự động phát hành trên COMMIT / ROLLBACK.
Trygve Laugstøl

Câu trả lời:


2

Giả sử bạn lưu trữ tất cả các sửa đổi của tài liệu trong một bảng, một cách tiếp cận sẽ là không lưu trữ số sửa đổi mà tính toán nó dựa trên số lần sửa đổi được lưu trữ trong bảng.

Về cơ bản, nó là một giá trị xuất phát , không phải là thứ bạn cần lưu trữ.

Một chức năng cửa sổ có thể được sử dụng để tính số sửa đổi, đại loại như

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

và bạn sẽ cần một cột giống như change_dateđể theo dõi thứ tự của các phiên bản.


Mặt khác, nếu bạn chỉ có revisionmột tài sản của tài liệu và nó cho biết "tài liệu đã thay đổi bao nhiêu lần", thì tôi sẽ sử dụng phương pháp khóa lạc quan, đại loại như:

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

Nếu điều này cập nhật 0 hàng, thì đã có cập nhật trung gian và bạn cần thông báo cho người dùng về việc này.


Nói chung, cố gắng giữ cho giải pháp của bạn đơn giản nhất có thể. Trong trường hợp này bởi

  • tránh sử dụng các chức năng khóa rõ ràng trừ khi thực sự cần thiết
  • có ít đối tượng cơ sở dữ liệu hơn (không có mỗi chuỗi tài liệu) và lưu trữ ít thuộc tính hơn (không lưu trữ bản sửa đổi nếu có thể tính được)
  • sử dụng một updatecâu lệnh thay vì selecttheo sau bởi một inserthoặcupdate

Thật vậy, tôi không cần lưu trữ giá trị khi nó có thể được tính toán. Cảm ơn vì đã nhắc tôi!
Julien Portalier

2
Trên thực tế, trong bối cảnh của tôi, một số phiên bản cũ hơn sẽ bị xóa vào một lúc nào đó, vì vậy tôi không thể tính toán được hoặc số sửa đổi sẽ giảm :)
Julien Portalier

3

SEQUENCE được đảm bảo là duy nhất và trường hợp sử dụng của bạn có thể áp dụng nếu số lượng tài liệu của bạn không quá cao (khác là bạn có rất nhiều trình tự để quản lý). Sử dụng mệnh đề RETURNING để lấy giá trị được tạo bởi chuỗi. Ví dụ: sử dụng 'A36' làm tài liệu_id:

  • Mỗi tài liệu, bạn có thể tạo một chuỗi để theo dõi mức tăng.
  • Quản lý các trình tự sẽ cần phải được xử lý một cách cẩn thận. Bạn có thể có thể giữ một bảng riêng có chứa tên tài liệu và trình tự được liên kết với bảng đó document_idđể tham chiếu khi chèn / cập nhật document_revisionsbảng.

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

Cảm ơn về định dạng deszo, tôi đã không nhận thấy rằng nó trông tệ như thế nào khi tôi dán vào bình luận của mình.
bma

Chuỗi là một bộ đếm xấu nếu bạn muốn giá trị tiếp theo là +1 trước khi chúng không chạy trong giao dịch.
Trygve Laugstøl

1
Hở? Trình tự là nguyên tử. Đó là lý do tại sao tôi đề xuất một trình tự cho mỗi tài liệu. Chúng cũng không được đảm bảo là không có khoảng cách, vì các rollback không làm tăng trình tự sau khi nó tăng lên. Tôi không nói rằng khóa thích hợp không phải là một giải pháp tốt, chỉ có điều đó trình bày một sự thay thế.
bma

1
Cảm ơn! Trình tự chắc chắn là cách để đi nếu tôi cần lưu trữ số sửa đổi.
Julien Portalier

2
Lưu ý rằng việc có số lượng lớn các chuỗi là một thành công lớn về hiệu suất, vì một chuỗi về cơ bản là một bảng có một hàng. Bạn có thể đọc thêm về điều đó ở đây
Magnuss

2

Điều này thường được giải quyết với khóa lạc quan:

SELECT version, x FROM foo;

version | foo
    123 | ..

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

Nếu bản cập nhật trả về 0 hàng được cập nhật, bạn đã bỏ lỡ bản cập nhật của mình vì người khác đã cập nhật hàng.


Cảm ơn! Đây là một điều tốt khi bạn cần theo dõi các cập nhật trên một tài liệu! Nhưng tôi cần một số sửa đổi duy nhất cho mỗi hàng trong bảng document_Vvutions, sẽ không được cập nhật và phải là người theo dõi bản sửa đổi trước đó (ví dụ: số sửa đổi của hàng trước + 1).
Julien Portalier

1
Hừm, tại sao bạn không thể sử dụng kỹ thuật này? Đây là phương pháp duy nhất (ngoài khóa bi quan) sẽ cung cấp cho bạn một chuỗi không có khoảng cách.
Trygve Laugstøl

2

. row_number())

Tôi có trường hợp sử dụng tương tự. Đối với mỗi bản ghi chèn vào một dự án cụ thể trong SaaS, chúng ta cần có một độc đáo, incrementing số có thể được tạo ra khi đối mặt với đồng thời INSERTs và là lý tưởng Gapless.

Bài viết này mô tả một giải pháp hay , mà tôi sẽ tóm tắt ở đây để dễ dàng và cho hậu thế.

  1. Có một bảng riêng đóng vai trò là bộ đếm để cung cấp giá trị tiếp theo. Nó sẽ có hai cột, document_idcounter. countersẽ là DEFAULT 0Cách khác, nếu bạn đã có một documentthực thể nhóm tất cả các phiên bản, một countercó thể được thêm vào đó.
  2. Thêm một BEFORE INSERTkích hoạt vào document_versionsbảng mà nguyên tử tăng bộ đếm ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter) và sau đó đặt NEW.versionthành giá trị bộ đếm đó.

Ngoài ra, bạn có thể sử dụng CTE để thực hiện việc này ở lớp ứng dụng (mặc dù tôi thích nó là một trình kích hoạt cho mục đích nhất quán):

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

Về nguyên tắc, điều này tương tự như cách bạn đã cố gắng giải quyết nó ban đầu, ngoại trừ việc sửa đổi một hàng truy cập trong một câu lệnh duy nhất, nó chặn các giá trị cũ cho đến khi INSERTcam kết.

Đây là một bản ghi từ psqlcho thấy điều này trong hành động:

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)

Như bạn có thể thấy, bạn phải cẩn thận về cách INSERTxảy ra, do đó phiên bản kích hoạt, trông như thế này:

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

Điều đó làm cho INSERTs thẳng hơn nhiều và tính toàn vẹn của dữ liệu mạnh hơn khi đối mặt với INSERTnguồn gốc từ các nguồn tùy ý:

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.