Skip to content
This repository has been archived by the owner on Nov 13, 2022. It is now read-only.

Latest commit

 

History

History
714 lines (479 loc) · 35.3 KB

08-mvcc-isolation-lock.md

File metadata and controls

714 lines (479 loc) · 35.3 KB

MVCC

개요

PostgreSQL은 내부적으로 다중 버전 동시성 제어(MVCC, Multi Version Concurrency Control) 모델을 사용하여 동시성을 제어합니다. 위키피디아가 하나의 문서마다 여러개의 버전을 가지고 있듯이, PostgreSQL은 각 행마다 여러개의 버전을 허용합니다.


MVCC모델은 하나의 테이블에 대한 READ, WRITE 동시 작업을 LOCK없이 수행할 수 있도록 도와주어, 데이터베이스 전체 성능을 향상시킵니다.


트랜잭션 아이디

트랜잭션(=BEGIN)이 시작되면 해당 트랜잭션에는 특별한 식별자가 부여됩니다. 이것을 트랜잭션 아이디(XID)라고 하는데, 해당 트랜잭션이 커밋되면 튜플에 XID를 함께 저장하기 때문에, 어떤 행이 언제 수정되었는지 XID를 통해 알아낼 수 있습니다. MVCCXID를 사용하여 자신보다 과거에 수행되었고 커밋된 트랜잭션의 결과만 확인할 수 있도록 강제하는 매커니즘입니다.


트랜잭션 아이디는 unsigned int32 타입의 정수이며, 타입의 최대값에 도달하면 3부터 재활용합니다. 0, 1, 2부터 시작하지 않는 이유는 특별한 상태를 표시하는 값이기 때문이고. 재활용이 발생하면 자신보다 값이 커도 과거일 수 있기 때문에, 환형으로 그렸을 때 자신보다 2^31개 앞에있는 아이디는 Future로, 자신보다 2^31개 뒤에있는 아이디는 Past로 간주됩니다.

  • TID 0 : 유효하지 않은 트랜잭션. (존재하지 않는 트랜잭션.)
  • TID 1 : 부트스트랩 트랜잭션.
  • TID 2 : 동결된 트랜잭션.


데이터는 항상 덧붙여진다

수정이 발생하더라도 이전 버전은 바로 삭제되지 않습니다. 아직 해당 버전을 사용중인 트랜잭션이 존재할 수 있기 때문입니다. 따라서 PostgreSQL은 수정이 발생해도 이전 버전을 삭제하지 않고 삭제 표시만 해둔 뒤에, 최신 버전을 파일의 뒤에 덧붙입니다. 이러한 방식 때문에, 파일의 크기는 항상 증가하는 방향으로 흘러갑니다.


다만 오라클은 이것과는 다른 방식으로 MVCC를 구현하는데, 해당 포스팅에서는 이에 대해 설명하지 않겠습니다.


튜플 헤더의 구조

각 튜플의 헤더는 MVCC를 구현하기 위한 4가지 컬럼이 정의되어 있습니다.

  • t_xmin : 이 버전을 생성한 트랜잭션의 아이디. (생성 시각)
  • t_xmax : 이 버전을 삭제한 트랜잭션의 아이디. (만료 시각)
  • t_cid : 이 버전을 생성한 트랜잭션에서, 몇 번째 쿼리가 이 버전을 생성했는지 가르킨다. (0부터 시작.)
  • t_ctid : 다음 버전의 (페이지번호, 컬럼번호)가 기록되어 있다. 현재 버전이 최신인 경우 자기 자신을 가르킨다.

MVCC 예제

튜플 삽입

XID=99인 트랜잭션이 새로운 데이터를 삽입했다면, 현재 페이지의 가장 뒤에 덧붙여집니다.

Tuple_1 :

column desc
t_xmin 해당 버전을 생성한 트랜잭션의 아이디인 99가 저장됩니다.
t_xmax 해당 버전을 삭제한 트랜잭션이 없으므로 0이 저장됩니다.
t_cid 해당 버전은 트랜잭션의 0번째 쿼리가 생성했습니다.
t_ctid 해당 버전이 최신이므로 자기 자신을 가르킵니다.

튜플 삭제

XID=111인 트랜잭션이 데이터를 삭제했다면, 실제 페이지에서는 지워지지 않고 논리적으로만 지워집니다.

Tuple_1 :

column desc
t_xmin
t_xmax 해당 버전을 삭제한 트랜잭션의 아이디인 111이 저장됩니다.
t_cid
t_ctid

이제 XID=111보다 미래의 트랜잭션이 이 테이블을 조회하면 Tuple_1은 조회되지 않습니다.


튜플 갱신

XID=100인 트랜잭션이 데이터를 갱신하면, 이전의 데이터는 유지한 채, 새로운 데이터를 페이지의 가장 뒷부분에 덧붙입니다.


UPDATE tbl SET data = 'B';

Tuple_1 :

column desc
t_xmin
t_xmax 다음 버전을 생성한 트랜잭션의 아이디인 100이 저장됩니다.
t_cid
t_ctid 다음 버전을 가르킵니다.

Tuple_2 :

column desc
t_xmin 해당 버전을 생성한 트랜잭션의 아이디인 100이 저장됩니다.
t_xmax 해당 버전을 삭제한 트랜잭션이 없으므로 0이 저장됩니다.
t_cid 해당 버전은 트랜잭션의 0번째 쿼리가 생성했습니다.
t_ctid 현재 버전을 가르킵니다.

UPDATE tbl SET data = 'C';

Tuple_2 :

column desc
t_xmin
t_xmax 다음 버전을 생성한 트랜잭션의 아이디인 100이 저장됩니다.
t_cid
t_ctid 다음 버전을 가르킵니다.

Tuple_3 :

column desc
t_xmin 해당 버전을 생성한 트랜잭션의 아이디인 100이 저장됩니다.
t_xmax 해당 버전을 삭제한 트랜잭션이 없으므로 0이 저장됩니다.
t_cid 해당 버전은 트랜잭션의 1번째 쿼리가 생성했습니다.
t_ctid 현재 버전을 가르킵니다.

트랜잭션 스냅샷

트랜잭션 스냅샷이란 활동중인 트랜잭션의 아이디를 모아놓은 데이터 집합입니다. 현재 트랜잭션 스냅샷은 다음 쿼리를 통해 확인할 수 있습니다.

SELECT txid_current_snapshot();

트랜잭션 스냅샷은 2~3개의 요소가 콜론을 통해 구분됩니다.

lower:upper:[between [, ...]]
  • xid < lower이라면 inactive입니다.
  • uppder <= xid이라면 active입니다.
  • xid in between_list이라면 active입니다.


트랜잭션 스냅샷은 Transaction Manager에 의해 관리되며, 트랜잭션이 시작될 때 마다 포함되고, 트랜잭션이 커밋되거나 중단될 때 마다 제외됩니다.


동결 상태

어떤 행이 매우 오랫동안 수정되지 않아 Future로 취급되는 문제를 Transaction Wraparound Problem이라고 합니다. 예를 들어, 어떤 행이 XID=100에 의해 삽입되었다고 가정하겠습니다. 즉, t_xmin=100인 상태입니다. 시간이 매우 오랫동안 흐르고 XID=2^31 +100 +1에 도달한 경우에, 이제 XID=100은 미래로 간주되므로 튜플을 삭제하지는 않았지만 invisible로 취급되어 데이터가 보이지 않게됩니다.

PostgreSQL은 이러한 문제를 해결하기 위해, 오랫동안 수정되지 않은 튜플은 FREEZE로 표기하여 어떠한 상황에서든 visible로 취급하는 매커니즘을 도입했습니다. 즉, XID=2인 트랜잭션은 어떠한 상황에서든 inactive, visible로 간주됩니다.


이러한 동결 프로세스는 Vaccum Process에 의해 실행되며, vacuum_freeze_min_age(default 50 million)동안 건드린적이 없는 튜플은 FREEZE상태로 변경됩니다. 동결 상태를 표현하는 방법은 버전에 따라 다른데, 과거 버전은 t_xmin=2으로 수정하는 방식을 사용하고, 최신 버전은 t_infomask에 동결상태를 가르키는 마스크를 씌웁니다.


참고 문헌


Transaction Isolation Level

격리성 수준과 락의 강도

트랜잭션이 데이터 작업을 수행하기 위해서는 Lock을 획득하는 작업이 먼저 수행되어야 합니다. 하지만 트랜잭션이 하나만 있는것은 아니죠. 여러개의 트랜잭션이 하나의 테이블에 동시에 접근할 때 Lock에 의한 충돌이 발생할 수 있습니다.


하지만 무턱대고 Lock의 강도를 낮추면 동시성은 증가하지만 의도하지 않은 현상, 즉 이상현상(Anomaly)이 발생할 가능성이 큽니다. 그렇다고 Lock의 강도를 높이면 이상현상은 줄어들겠지만, 경합이 발생할 가능성이 더욱 늘어나기 때문에 동시성은 감소하겠죠.


상황에 따라 적절한 Trade-Off가 필요하며, 트랜잭션 격리성 레벨이 바로 Lock의 강도를 조절하는 역할을 수행합니다. 트랜잭션 격리성 레벨의 종류는 다음과 같습니다. 하위 레벨은 상위 레벨을 포함합니다.

level desc
Read uncommitted 커밋하지 않은 데이터도 읽거나 수정할 수 있음.
Read committed (default) 커밋된 데이터만 읽거나 수정할 수 있음.
Repeatable read 동시에 어떤 테이블이 변경(INSERT or UPDATE or DELETE)되는 것을 방지.
Serializable 동시에 어떤 테이블이 변경&조회((INSERT or UPDATE or DELETE) and (SELECT)) 되는 것을 방지.

트랜잭션 레벨을 조절하려면, BEGIN 이후에 다음 쿼리를 수행해야 합니다.

SET TRANSACTION ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }

이상현상

잠금의 강도가 낮아 의도치 않은 결과가 발생하는 것을 Anomaly라고 합니다. 이상현상의 종류는 다음과 같습니다.

anomaly desc
Dirty Read 커밋되지 않은 데이터가 보임.
Nonrepeatable Read 연속으로 테이블을 읽었는데 서로 결과가 다름.
Phantom Read 연속으로 테이블을 읽었는데 없었던 행이 추가됨.
Serialization Anomaly 같은 상황에서 동일한 트랜잭션 그룹의 수행결과가 매번 달라짐.

※ 하위에 있을수록 특별한 케이스의 이상현상이며, 상위에 있는 이상현상은 하위의 이상현상을 포함합니다.


각 격리성 레벨로 해결할 수 있는 이상현상은 다음과 같습니다.

Isolation Level Dirty Read Nonrepeatable Read Phantom Read Serialization Anomaly
Read uncommitted 가능, PG에서는 불가 가능 가능 가능
Read committed 불가능 가능 가능 가능
Repeatable read 불가능 불가능 가능, PG에서는 불가 가능
Serializable 불가능 불가능 불가능 불가능

Dirty Read

커밋되지 않은 튜플이 보이는 현상입니다. 다만 PostgreSQL은 모든 격리성 레벨에서 inactive 트랜잭션의 변경점을 볼 수 없기 때문에, 어떠한 격리성 레벨에서든 Dirty Read가 발생하지 않습니다.

※ 표준에서는 READ COMMITTED 이상을 사용해야 해결되도록 명시하고 있습니다.


아래는 PostgreSQLDirty Read를 허용한다고 가정했을 경우의 시나리오입니다.

CREATE TABLE Point2D (
    x int,
    y int
);
tx1 tx2
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM Point2D;
x ㅣ y
--------
0 rows affected.
INSERT INTO Point2D VALUES (0, 0);
SELECT * FROM Point2D;
x ㅣ y
--------
0 ㅣ 0
1 rows affected.
ROLLBACK;
SELECT * FROM Point2D;
x ㅣ y
--------
0 rows affected.
COMMIT;

Nonrepeatable Read

테이블을 연속으로 읽었는데, 내용이 변경된 행이 발견되는 현상입니다. 두 번의 조회 중간에 다른 트랜잭션이 데이터를 변경했을 때 발생합니다. READ COMMITTED이하의 격리성 레벨은 자신보다 이후에 일어난 inactive 트랜잭션의 변경점도 읽기 때문에 발생합니다. REPEATABLE READ 이상의 격리성 레벨을 사용하면, 테이블의 동시 갱신을 방지하므로 해결할 수 있습니다.

※ 다른 데이터베이스는 행에 락을 거는 방식으로 구현합니다.

CREATE TABLE Point2D (
    x int,
    y int
);

INSERT INTO Point2D VALUES (0, 0);
tx1 tx2
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM Point2D;
x ㅣ y
--------
0 ㅣ 0
1 rows affected.
UPDATE Point2D SET (x, y) = (1, 1);
COMMIT;
SELECT * FROM Point2D;
x ㅣ y
--------
1 ㅣ 1
1 rows affected.
COMMIT;

Phantom Read

테이블을 연속으로 읽었는데, 새롭게 추가된 행이 발견되는 현상입니다. 두 번의 조회 중간에 다른 트랜잭션이 데이터를 삽입했을 때 발생합니다. READ COMMITTED이하의 격리성 레벨은 자신보다 이후에 일어난 inactive 트랜잭션의 변경점도 읽기 때문에 발생합니다. REPEATABLE READ 이상의 격리성 레벨을 사용하면, 자신보다 이후에 일어난 inactive 트랜잭션을 읽지 않으므로 해결할 수 있습니다.

※ 다른 데이터베이스는 테이블에 락을 거는 방식으로 구현합니다.

※ 표준에서는 SERIALIZABLE를 사용해야 해결되도록 명시하고 있습니다.


CREATE TABLE Point2D (
    x int,
    y int
);
tx1 tx2
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM Point2D;
x ㅣ y
--------
0 rows affected.
INSERT INTO Point2D VALUES (0, 0);
COMMIT;
SELECT * FROM Point2D;
x ㅣ y
--------
0 ㅣ 0
1 rows affected.
COMMIT;

Write-Write Confilct

REPEATABLE READ 이상의 격리성 수준은 테이블의 동시 갱신(UPDATE)을 배제합니다.


두 개의 쓰기 트랜잭션이 같은 행을 동시에 수정하는 상황을 가정해보면, 더 빨리 UPDATE문에 도달한 트랜잭션은 테이블에 대한 배타적 락을 성공적으로 획득하고, 뒤늦게 UPDATE문에 도달한 트랜잭션은 테이블에 대한 배타적 락을 얻으려고 시도하지만, 첫 번째 트랜잭션이 얻은 락과 충돌이 발생하므로 선행 트랜잭션이 COMMIT 또는 ROLLBACK을 날려서 락이 해제될 때 까지 대기합니다. (이것은 모든 격리성 레벨에서 적용됩니다.)


이 때, 선행 트랜잭션이 ROLLBACK을 날린다면 후행 트랜잭션의 UPDATE문은 성공하지만, 선행 트랜잭션이 COMMIT을 날린다면 후행 트랜잭션은 실패하게 됩니다. 후행 트랜잭션이 갱신하고자 하는 튜플이 더 이상 유효하지 않기 때문입니다.

CREATE TABLE Point2D (
    x int,
    y int
);

INSERT INTO Point2D VALUES (0, 0);
tx1 tx2
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
UPDATE Point2D SET (x, y) = (1, 1);
UPDATE Point2D SET (x, y) = (2, 2); -- 대기
COMMIT;
UPDATE Point2D SET (x, y) = (2, 2); -- 에러
ERROR:  could not serialize access due to concurrent update

오류:  동시 업데이트 때문에 순차적 액세스가 불가능합니다.

해당 에러가 발생하면, 실패한 트랜잭션을 처음부터 다시 시도해야 합니다.


반면 선행 트랜잭션이 ROLLBACK을 날리면, 후행 트랜잭션이 갱신하고자 하는 튜플이 여전히 유효하므로 에러가 발생하지 않습니다.

tx1 tx2
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
UPDATE Point2D SET (x, y) = (1, 1);
UPDATE Point2D SET (x, y) = (2, 2); -- 대기
ROLLBACK;
UPDATE Point2D SET (x, y) = (2, 2); -- 성공
COMMIT;

격리성 수준이 READ COMMITTED 이하인 경우 COMMIT을 날려도 에러가 발생하지 않습니다. 해당 격리성 수준에서는 Nonrepeatable Read가 발생하므로, 후행 트랜잭션이 선행 트랜잭션의 변경점을 볼 수 있기 때문입니다. 다만, 후행 트랜잭션이 선행 트랜잭션의 결과를 덮어쓰는 Lost Update현상이 발생할 수 있습니다.

tx1 tx2
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
UPDATE Point2D SET (x, y) = (1, 1);
UPDATE Point2D SET (x, y) = (2, 2); -- 대기
COMMIT;
UPDATE Point2D SET (x, y) = (2, 2); -- 성공
COMMIT;

tx2tx1의 내용을 덮었기 때문에, 최종 결과는 다음과 같습니다.

x y
2 2

Serialization Anomaly

선행 트랜잭션의 진행 중에 다른 트랜잭션이 끼어드는 바람에, 선행 트랜잭션이 일부 튜플을 누락한 현상을 직렬화 이상이라고 합니다. 시나리오를 가정해보겠습니다. 아래의 순서도에서 각각의 SELECT COUNT(*) FROM Point2D; 은 어떤 값을 출력할까요? 초기 테이블은 비어있다고 가정합니다.

CREATE TABLE Point2D (
    x int,
    y int
);
tx1 tx2
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
INSERT INTO Point2D VALUES (1, 1);
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
INSERT INTO Point2D VALUES (2, 2);
SELECT COUNT(*) FROM Point2D; -- 1
COMMIT;
SELECT COUNT(*) FROM Point2D; -- 1
COMMIT;

tx1 관점에서 살펴보겠습니다. REPETABLE READ 레벨에서는 후행 트랜잭션의 결과가 보이지 않기 때문에 1을 출력하겠지만, SELECT가 실행되기 전에 후행 트랜잭션이 (2, 2)을 삽입하고 커밋까지 해버린 것을 눈여겨봐야 합니다. 정말로 결과가 1이 맞는걸까요? SELECT가 실행된 시점에서는 데이터베이스에 2개의 튜플이 존재하고 있을텐데 말이죠. 모든 트랜잭션이 끝나고 다시 SELECT COUNT(*) FROM Point2D;를 날리면 2가 나올 것임이 명백합니다.


위와 같은 이상현상은 동일 테이블에 읽기와 쓰기를 시도하려는 트랜잭션이 2개 이상 있을 경우에 발견됩니다. SERIALIZABLE은 위와같은 직렬화 이상을 감지하면, 선착순 하나만 커밋을 허용하고, 나머지는 커밋을 거부합니다. 이것은 N개의 경쟁자 중 오직 하나만 읽기/쓰기가 허락된다고 볼 수 있습니다. SERIALIZABLE이 커밋을 거부하면, 실패한 트랜잭션을 처음부터 다시 시도해야 하며, 이것을 성공할 때 까지 반복해야 합니다.

ERROR:  could not serialize access due to read/write dependencies among transactions

오류:  트랜잭션간 읽기/쓰기 의존성 때문에 serialize 접근을 할 수 없음
상세정보:  Reason code: Canceled on identification as a pivot, during commit attempt.
힌트:  재시도하면 그 트랜잭션이 성공할 것입니다.


Lock

테이블 락

테이블을 조회하거나 정의를 변경하는 경우에 테이블에 대한 락을 먼저 획득해야 합니다. 배타 락은 하나의 트랜잭션만이 소유할 수 있으며, 공유 락은 여러개의 트랜잭션이 동시에 소유할 수 있습니다.


PostgreSQL에서 지원하는 테이블 락은 다음과 같습니다. 이름에 ROW가 들어있더라도 테이블 락입니다.


ACCESS SHARE

충돌 :

  • ACCESS EXCLUSIVE

가장 너그러운 락입니다. 가장 강력한 락인 ACCESS EXCLUSIVE을 제외한 모든 락과 호환됩니다. 테이블을 읽고 수정하지 않는 명령어(SELECT)가 해당 락을 요구합니다.


ROW SHARE

충돌 :

  • EXCLUSIVE
  • ACCESS EXCLUSIVE

명시적인 SELECT FOR UPDATE 또는 SELECT FOR SHARE 명령어로만 획득할 수 있습니다.


ROW EXCLUSIVE

충돌 :

  • SHARE
  • SHARE ROW EXCLUSIVE
  • EXCLUSIVE
  • ACCESS EXCLUSIVE

테이블의 데이터를 수정하거나 삽입하기 위해 필요합니다. UPDATE, DELETE, INSERT와 같이 테이블의 데이터를 수정하는 명령어가 해당 락을 요구합니다. INSERT의 경우에 외부키가 설정되어 있다면, 외부 테이블에 대한 ACCESS SHARE락도 함께 요구합니다. 여러 트랜잭션이 하나의 테이블의 서로다른 범위를 수정할 수 있으므로, 자기 자신과는 충돌하지 않습니다.


SHARE UPDATE EXCLUSIVE

충돌 :

  • SHARE UPDATE EXCLUSIVE
  • SHARE
  • SHARE ROW EXCLUSIVE
  • EXCLUSIVE
  • ACCESS EXCLUSIVE

테이블의 스키마를 변경하기 위해 필요합니다. 다음 명령어들이 해당 락을 요구합니다.

  • VACUUM WITHOUT FULL
  • ANALYZE
  • CREATE INDEX CONCURRENTLY
  • REINDEX CONCURRENTLY
  • CREATE STATISTICS
  • 일부 ALTER INDEX
  • 일부 ALTER TABLE

SHARE

충돌 :

  • ROW EXCLUSIVE
  • SHARE UPDATE EXCLUSIVE
  • SHARE ROW EXCLUSIVE
  • EXCLUSIVE
  • ACCESS EXCLUSIVE

테이블의 데이터가 추가, 삭제, 변경되지 않도록 막습니다. CREATE INDEX WITHOUT CONCURRENTLY이 해당 락을 요구합니다.


SHARE ROW EXCLUSIVE

충돌 :

  • ROW EXCLUSIVE
  • SHARE UPDATE EXCLUSIVE
  • SHARE
  • SHARE ROW EXCLUSIVE
  • EXCLUSIVE
  • ACCESS EXCLUSIVE

테이블의 데이터가 추가, 삭제, 변경되지 않도록 막습니다. 하나의 세션만 보유할 수 있습니다. CREATE TRIGGER 또는 ALTER TABLE이 해당 락을 요구합니다.


EXCLUSIVE

충돌 :

  • ROW SHARE
  • ROW EXCLUSIVE
  • SHARE UPDATE EXCLUSIVE
  • SHARE
  • SHARE ROW EXCLUSIVE
  • EXCLUSIVE
  • ACCESS EXCLUSIVE

이 락은 ACCESS SHARE만 호환됩니다. 즉, SELECT문장만 이 락과 호환됩니다. REFRESH MATERIALIZED VIEW CONCURRENTLY가 해당 락을 요구합니다.


ACCESS EXCLUSIVE

충돌 :

  • ACCESS SHARE
  • ROW SHARE
  • ROW EXCLUSIVE
  • SHARE UPDATE EXCLUSIVE
  • SHARE
  • SHARE ROW EXCLUSIVE
  • EXCLUSIVE
  • ACCESS EXCLUSIVE

이 락은 모든 락과 충돌됩니다. 해당 락을 얻었다는 것은 그 테이블에 액세스하는 유일한 트랜잭션임을 보장합니다.


다음 명령어들이 해당 락을 요구합니다.

  • DROP TABLE
  • TRUNCATE
  • REINDEX
  • CLUSTER
  • VACUUM FULL
  • REFRESH MATERIALIZED VIEW WITHOUT CONCURRENTLY
  • 대부분의 ALTER INDEX
  • 대부분의 ALTER TABLE

호환성 테이블


로우 락

각 행은 갱신, 삭제, 조회를 방지하기 위한 락이 설정될 수 있습니다. 여러개의 잠금이 동시에 설정될 수 있지만 충돌하지 않는 락으로만 구성되어야 합니다. 행 잠금의 종류는 다음과 같으며, 위에 있을수록 더 강력한 잠금입니다.

  • UPDATE
  • NO KEY UPDATE
  • SHARE
  • KEY SHARE

UPDATE

갱신 또는 삭제를 위한 배타적 잠금입니다. 배타적 잠금은 타 트랜잭션의 아래의 구문을 블럭합니다.

  • UPDATE
  • DELETE
  • SELECT FOR UPDATE
  • SELECT FOR NO KEY UPDATE
  • SELECT FOR SHARE
  • SELECT FOR KEY SHARE

NO KEY UPDATE

기본적으로는 FOR UPDATE와 동일하지만 조금 더 약합니다. SELECT FOR KEY SHARE를 블럭하지 않습니다. 이 잠금은 FOR UPDATE를 사용하지 않은 UPDATE구문에 의해서도 획득됩니다. 즉, 타 트랜잭션의 아래의 구문을 블럭합니다.

  • UPDATE
  • DELETE
  • SELECT FOR UPDATE
  • SELECT FOR NO KEY UPDATE
  • SELECT FOR SHARE

SHARE

기본적으로는 NO KEY UPDATE와 동일하지만 조금 더 약합니다. 다만, 위에서 소개된 잠금이 배타적 잠금을 걸었다면 이하의 잠금은 공유적 잠금을 겁니다. 공유적 잠금은 타 트랜잭션의 아래의 구문을 블럭합니다.

  • UPDATE
  • DELETE
  • SELECT FOR UPDATE
  • SELECT FOR NO KEY UPDATE

SELECT FOR KEY SHARE

기본적으로는 FOR SHARE와 동일하지만 조금 더 약합니다. SELECT FOR SHARE를 블럭하지 않습니다. 즉, 타 트랜잭션의 아래의 구문을 블럭합니다.

  • UPDATE
  • DELETE
  • SELECT FOR UPDATE

호환성 테이블

※ 모든 락은 UPDATEDELETE를 블럭합니다.


Dead Lock

데드락을 감지한 경우, 후행 트랜잭션을 취소시키고 선행 트랜잭션을 허용합니다.

CREATE TABLE Data1 (
    n int
);

CREATE TABLE Data2 (
    n int
);
tx1 tx2
BEGIN;
SELECT * FROM Data1;
BEGIN;
SELECT * FROM Data2;
DROP TABLE Data2; -- tx2의 ACCESS SHARE가 해제될 때 까지 대기
DROP TABLE Data1; -- tx1의 ACCESS SHARE가 해제될 때 까지 대기
ERROR
DROP TABLE Data2; -- OK
COMMIT;