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

주어진 작업 (key2)에 대해 문서 행 (key1)을 대신 잠그면 안됩니까? 따라서 적절한 해결책이 될 것입니다.

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대신 에 단일 문 사용selectinsertupdate

실제로, 나는 계산할 수있을 때 값을 저장할 필요가 없습니다. 상기시켜 주셔서 감사합니다!
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_idcounter. counter될 것입니다 DEFAULT 0당신이 이미있는 경우, 또는 document그룹의 모든 버전하는 그 실체 counter가 추가 될 수 있습니다.
  2. 카운터 ( ) 를 원자 적으로 증가시킨 다음 해당 카운터 값으로 설정 하는 BEFORE INSERT트리거를 document_versions테이블에 추가하십시오 .UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counterNEW.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.