Access Method
PostgreSQL 디자인 문서를 살펴보면 확장성을 특히 유념하고 설계되었음을 알 수 있습니다. 그 결과, PostgreSQL의 코어 시스템은 구현되어있는 Access Method를 호출할 뿐 실제 해당 내용에 대해서는 거의 알지 못하는 구조를 가지고 있습니다. 즉, Access Method는 상당한 독립성을 가지고 있어 여러 개발자가 쉽게 추가할 수 있도록 하는 것을 의도한 것입니다.
PostgreSQL 곳곳에서 강조하고 있는 내용인 만큼 우리는 PostgreSQL의 Data Access에 대해 이해하기 위해서는 Access Method에 대한 이해가 필요합니다.
일단, 다음 명령어를 통해 PostgreSQL에 기본으로 내장된 Access Method를 살펴봅시다. amtype을 보면 t (table), i (index) 타입의 Access Method가 기본적으로 존재함을 확인할 수 있습니다. 테이블과 인덱스의 개념은 본문에서 설명하지 않겠습니다.
postgres=# SELECT * FROM PG_AM;
oid | amname | amhandler | amtype
——+————-+————————————-+————–
2 | heap | heap_tableam_handler | t
403 | btree | bthandler | i
405 | hash | hashhandler | i
783 | gist | gisthandler | i
2742 | gin | ginhandler | i
4000 | spgist | spghandler | i
3580 | brin | brinhandler | i
(7 rows)
Table Access Method
이전 장들에서 우리는 Storage for Tables 즉, 실제로 데이터베이스의 테이블 등이 파일 시스템에서 어떻게 저장되는지 설명한 바 있습니다. Table Access Method는 어떻게 이에 접근하고 관리할지에 대한 내용을 정의하고 있습니다.
Index Access Method
Index Access Method 또한 Table Access Method와 크게 다르지 않습니다. 대부분의 관계형 데이터베이스는 탐색 속도를 향상하기 위하여 인덱스라는 개념을 활용하고 있습니다. Index Access Method는 각 Index Type을 통해 어떻게 데이터에 접근하고 관리하는지에 대한 내용을 정의하고 있습니다.
Scan
데이터베이스는 쿼리 응답에 필요한 데이터를 찾기 위해 Scan이라는 과정을 거칩니다. PostgreSQL은 Access Method에 따라 Sequential Scan, Index Scan, Bitmap Scan 등의 전략을 사용합니다. 특정 쿼리에서 어떤 Scan 전략을 사용했는지는 EXPLAIN을 사용하여 쿼리를 수행했을 때, 플랜 노드의 형태로 다음과 같이 나타납니다.’
postgres=# EXPLAIN SELECT * FROM PG_AM;
QUERY PLAN
————————————————————————
Seq Scan on pg_am (cost=0.00..1.07 rows=7 width=73)
(1 row)
Sequential Scan
Sequential Scan은 PostgreSQL 데이터 구조에서 언급한 바 있습니다. Sequential Scan은 말 그대로 순차 스캔입니다. 테이블이 저장되어 있는 파일의 처음부터 끝 까지 순차적으로 탐색하면서 쿼리 조건(Predicate)에 맞는 데이터를 찾습니다. 가장 단순한 형태의 전략인만큼 단점은 명확합니다. 10000개의 데이터 중 1개의 데이터를 찾기 위해 10000개에 해당하는 튜플을 모두 탐색해야하므로 테이블 크기에 비례하여 탐색 비용이 증가합니다.
Index Scan
Index Scan 역시 PostgreSQL 데이터 구조에서 언급한 바 있습니다. 우리는 Sequential Scan 전략의 단점에 대해서 상술하였습니다. 해당 문제를 개선하기 위하여 PostgreSQL을 포함한 대부분의 데이터베이스는 중간에 B-TREE와 같은 탐색 알고리즘을 끼워넣었습니다. Index Scan은 쿼리 요청이 들어오면 쿼리 조건에 맞는 인덱스 튜플을 해당 인덱스의 타입에 따라 탐색하여 찾아냅니다. 인덱스 튜플은 실제 데이터를 포함하고 있는 힙 튜플을 가리키고 있으므로 이를 통해 원하는 데이터를 찾아냅니다.
Index Scan에 대해 요약하자면 적절한 탐색 알고리즘을 사용하여 시간 복잡도를 줄이고, 대신 인덱스 튜플에서 힙 튜플을 한번더 찾아가는 비용을 감수했다고 할 수 있습니다.
“적적한 탐색 알고리즘”이라는 말은 실제 데이터베이스 운영환경에서 해당 데이터를 주로 어떤 패턴으로 탐색하느냐에 따라 “적절한 Index Scan 방식”을 사용해야 한다는 것을 의미합니다.
Enhance Access Performance
PostgreSQL은 데이터 접근에 대한 효율을 올리기 위하여 몇 가지 특별한 방식을 제공하고 있습니다. 이번 장에서는 그 중 Index Only Scan과 Heap Only Tuple이라는 방식을 설명하겠습니다.
Index Only Scan
PostgreSQL의 각 인덱스는 테이블의 데이터 영역(Heap)과 별도로 저장됩니다. 따라서, 인덱스의 탐색 결과로 얻을 수 있는 인덱스 튜플이 가리키는 실제 데이터를 포함하는 힙 튜플에 한번 더 접근해야만 마침내 원하는 데이터를 얻을 수 있습니다.
PostgreSQL에서는 힙 튜플에 한번 더 접근하지 않고 인덱스에서만 쿼리에 대한 답변을 구성할 수 있는 Index Only Scan 기능을 제공합니다. 즉, Index Only Scan 기능은 인덱스에서 직접 값을 반환합니다.
당연하게도 인덱스에서만 쿼리에 대한 답변을 구성할 수 있는 상황은 쿼리가 요청하는 대상이 인덱스 키가 전부인 경우에 한정됩니다.
그러나 우리는 Concurrency Control에서 배웠듯이 PostgreSQL의 경우, 원칙적으로 튜플의 Visibility를 확인해야 합니다. 결국 해당 인덱스 튜플의 Visibility를 확인하기 위하여 테이블 데이터에 접근해야하는데… PostgreSQL은 이 상황을 피하기 위해 Index Only Scan을 고안하였습니다. 따라서, 위와 같은 상황이라면 힙 튜플에 접근하지 않겠다는 Index Only Scan 아이디어의 이점을 온전히 얻기 어렵습니다.
이를 해결하기 위해 PostgreSQL은 다음과 같은 중재안을 사용합니다. 만약 대상 페이지에 포함된 모든 튜플이 Visible하다면 굳이 Visibility Check를 위해 테이블 페이지에 더 이상 접근하지 않습니다. 그 외에는 힙 튜플을 읽어서 Visibility를 확인합니다.
Heap Only Tuple
우리는 Concurrency Control 장에서 PostgreSQL이 MVCC(Multi-Version Concurrency Control)을 어떻게 구현하는지에 대하여 설명하였습니다.
PostgreSQL의 UPDATE는 꽤나 값 비싼 작업이 될 수 있습니다. 일단 다음 사실을 한번 다시 한번 상기해봅시다.
- PostgreSQL의 인덱스는 테이블의 데이터 영역과는 별도로 저장되어 있습니다.
- PostgreSQL의 MVCC 방식에 의해 데이터 갱신이 발생할 때마다 기존의 튜플은 구 버전의 데이터가 되고 갱신된 값은 새로운 튜플에 담깁니다.
위 사실에 의해 데이터가 수정 될 때마다, 인덱스가 가리키던 튜플은 구 버전의 튜플이 되기 때문에 인덱스 엔트리를 갱신해야 함을 알 수 있습니다.
Operation
이제 예시를 통해 Heap Only Tuple의 동작 원리를 이해해보겠습니다.
일단 다음 예시를 통해 Heap Only Tuple을 사용하지 않는 경우를 우선 살펴봅시다.

다음 예시는 Heap Only Tuple을 사용할 경우의 동작입니다. Heap Only Tuple의 목적 중 “인덱스 엔트리를 갱신하지 않도록한다”에 집중에서 살펴보길 바랍니다.

아래 표는 위 예시에서 실제 튜플의 값들이 어떻게 변하는지 보여주고 있습니다.
Tup.No. | t_xmin | t_xmax | t_ctid | t_informask2 | data |
---|---|---|---|---|---|
1 | 99 | 0 | (5,1) | A |
A를 업데이트하여 B로 변경 이후, 새로운 버전의 튜플인 2번 튜플이 새로 생성되고 기존의 1번 튜플에는 HEAP_HOT_UPDATED가 t_informask2에 마크된 것을 확인할 수 있습니다. 2번 튜플 역시, HEAP_ONLY_TUPLE이 마크되어 있습니다.
Tup.No. | t_xmin | t_xmax | t_ctid | t_informask2 | data |
---|---|---|---|---|---|
1 | 99 | 100 | (5,2) | HEAP_HOT_UPDATED | A |
2 | 100 | 0 | (5,2) | HEAP_ONLY_TUPLE | B |
위 예시를 통해 UPDATE가 발생하였음에도 불구하고 해당 튜플에 HEAP_HOT_UPDATED 마크가 되어있으면 t_ctid를 통해 다음 튜플을 찾아가는 방식(Index Tuple → 1 → Tuple 1 → Tuple 2)으로 인덱스 갱신 없이 새로운 버전의 데이터를 찾아갈 수 있음을 확인하였습니다.
우리는 Vacuum에서 데드 튜플에 대하여 설명하였습니다. 결국 1번 튜플은 데드 튜플이 될 것입니다. 그렇다면 해당 인덱스는 2번 튜플을 찾아갈 수 없게 됩니다. 이를 해결하기 위해 1번 튜플이 데드 튜플이 되면 PostgreSQL은 1번 튜플의 라인 포인터를 1번 튜플이 아닌 2번 튜플의 라인 포인터를 가리키도록 수정합니다(Redirection). 이를 프루닝(Pruning)이라 합니다. 아래 그림은 프루닝을 수행한 결과 예시입니다.

프루닝 결과, 해당 인덱스 튜플은 Index Tuple → 1 → 2 → Tuple 2 순서로 데이터를 찾아갑니다.
Vacuum에서 배웠듯이 Heap Only Tuple 역시, 1번 튜플을 계속 저 상태로 두지는 않습니다. PostgreSQL은 적절한 시점에 1번 튜플을 완전히 제거하고 튜플들을 재배치합니다. 이러한 과정을 조각모음(Defragmentation)이라고 합니다. 해당 과정은 간단합니다. 이미 프루닝 과정을 통해 Redirection이 되어 있기 때문에 단순히 Tuple 1만 제거해주면 됩니다.
조각모음과 Vacuum과 비교하면 인덱스 튜플을 제거하는 과정이 없다는 것을 알 수 있습니다. 그렇기 때문에 실제로 비용 또한 적게 소요됩니다.
Constraint
Heap Only Tuple을 항상 사용할 수 있는 것은 아닙니다.
- 새로운 튜플이 이전 버전의 튜플과 다른 페이지에 생성된다면 Heap Only Tuple을 사용할 수 없습니다.
- 인덱스 튜플의 키 값이 갱신된다면 새로운 인덱스 튜플을 추가해야 합니다.
Reference.
https://www.postgresql.org/docs/current/index-scanning.html
https://www.postgresql.org/docs/current/storage-hot.html
https://www.postgresql.org/docs/current/indexes-index-only-scans.html
지금까지 PostgreSQL의 Data Access에 관해 알아보았습니다
‘PostgreSQL의 Buffer Manager’를 바로 이어서 확인해보세요!