DBMS 구조·질의최적화·동시성·복구
문구점의 주문표·키·자료형과 인덱스 후보는 앞 절에서 정했다. 고객 C1은 최근 주문을 기다리고, 두 계산대는 마지막 재고 한 개를 동시에 주문하려 한다. 그때 서버가 재시작한다면 어떤 주문이 이미 확정됐고 어떤 주문이 취소돼야 할까?
먼저 읽을 흐름과 선수 지식
같은 주문표에서 느림·동시 주문·장애가 만난다
문구점의 주문표·키·자료형과 인덱스 후보는 앞 절에서 정했다. 고객 C1은 최근 주문을 기다리고, 두 계산대는 마지막 재고 한 개를 동시에 주문하려 한다. 그때 서버가 재시작한다면 어떤 주문이 이미 확정됐고 어떤 주문이 취소돼야 할까?
세 질문은 연결되어 있다. 적절한 읽기 경로를 골라도 다른 작업이 변경 중인 값을 잘못 보면 결과가 틀리고, 메모리에서 값을 바꿨더라도 완료를 확인한 변경이 장애 뒤 사라지면 주문을 믿을 수 없다. 이 절은 문구점에서 일어날 수 있는 설명용 상황을 따라 질의를 이해하고 실행하는 경로, 여러 작업의 간섭을 통제하는 경로, 실패 뒤 확정 상태를 되찾는 경로를 함께 읽는다. 실제 서비스에 장애를 일으키거나 명령을 따라 실행하는 실습은 없다.
이미 정한 뜻과 규칙을 가져온다
데이터베이스 관리시스템(DBMS)·질의·인덱스에서 DB(Database)는 관리할 데이터, DBMS(Database Management System)는 그 데이터의 정의·저장·질의·동시성·복구를 맡는 소프트웨어이며 SQL(Structured Query Language)은 원하는 결과를 표현하는 언어였다. 트랜잭션의 네 성질(ACID)에서 트랜잭션은 함께 성공하거나 실패할 논리적 작업이고, 커밋은 완료 확정, 롤백은 아직 확정되지 않은 변경 취소였다. 물리 DB 설계·인덱스·파티션의 DB 페이지와 인덱스는 행을 저장하고 찾아가는 구조였으며, 고객별 최신 목록의 인덱스도 읽기·쓰기 비용을 대조할 후보였다는 점을 가져온다.
DBMS 내부구조는 여러 책임이 협력하는 방식이다
DBMS 내부구조는 질의 해석·실행계획 선택·행 접근·메모리 데이터 관리·동시 작업 통제·변경 기록·복구가 서로 협력해 데이터의 정확한 이용을 지원하는 구조다. 구조도에 프로그램 이름을 많이 적는 것이 목적이 아니다. 어떤 정보를 받아 무엇을 판단하고, 어떤 상태를 남겨 다음 동작이 안전해지는지를 보아야 한다.
읽기에서는 어떤 행을 어떤 순서로 찾고 무엇을 볼 수 있는지가 중요하다. 변경에서는 어느 작업의 값인지, 언제 확정됐는지, 변경 기록과 실제 데이터가 어떤 순서로 저장되는지가 중요하다. 실패 뒤에는 이 기록으로 유지할 완료와 취소할 미완료를 판별한다. 모든 제품이 같은 프로그램·프로세스 배치를 쓰는 것은 아니며, 구체 구현은 PostgreSQL 18을 중심으로 판을 명시해 구별한다.
문장에서 결과까지의 길과 그 길을 지키는 장치
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
윗줄에서 SQL의 뜻을 확인하고 재작성한 뒤 실행 방법을 고른다. 아래쪽으로 이어지는 실행·결과 경로와 카탈로그·통계 입력을 구분한다. 저장·동시성·복구의 협력은 바로 다음 도식에 이어진다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
행 처리와 버퍼·동시성은 함께 참여하며, 데이터 파일을 반영하기 전에 필요한 변경 로그를 지속해야 한다. 기록으로 장애 뒤 상태를 복원한다. 양방향 선은 협력 관계이고 모든 화살표를 한 번씩 실행하는 시간표가 아니다. 결과는 앞 도식의 실행기를 통해 전달하며 데이터 파일 자체가 애플리케이션에 응답하는 뜻이 아니다.
가운데 아래 화살표를 반드시 한 번씩 차례로 실행한다는 고정 시간표로 읽지 않는다. 예를 들어 읽는 동안에도 어떤 작업의 값을 볼 수 있는지 확인하며, 바뀐 페이지를 저장하기 전에는 관련 변경 기록의 저장 순서를 지켜야 한다. 뒤에서 각각의 조건을 분리해 배운다. 지금은 값을 찾는 경로와 정확성을 지키는 경로가 같은 실행에 함께 참여한다는 관계를 잡는다.
SQL의 뜻에서 실행계획까지
읽을 문장 하나에 결과의 조건을 붙인다
앞 절 문구점의 고객별 최신 목록을 다음과 같은 문장으로 적을 수 있다. 표와 열의 이름은 앞 절의 업무 이름을 그대로 사용한 설명용 표현이며, 큰따옴표는 그 이름을 하나의 식별자로 적는 표시다. 작은따옴표의 'C1'은 찾으려는 고객번호 값이다.
SELECT "주문번호", "주문시각"
FROM "주문"
WHERE "고객번호" = 'C1'
ORDER BY "주문시각" DESC, "주문번호" DESC
LIMIT 20;FROM은 읽을 표, WHERE는 남길 행의 조건, SELECT는 출력할 열이다. ORDER BY는 결과 순서이고 DESC는 큰 값부터의 내림차순이다. LIMIT 20은 결과를 최대 20행으로 제한한다. 같은 주문시각이면 주문번호로 순서를 더 정한다. 최근 90일 조건을 여기서는 빼고 고객·정렬·개수만 읽으며, 90일 업무 요구를 폐기한 것이 아니라 문장 해석의 예를 작게 만든 것이다.
문장이 원하는 결과는 명확해도 모든 주문을 읽고 정렬할지, 맞는 인덱스 순서를 따라갈지는 적혀 있지 않다. 애플리케이션은 DBMS와 연결해 문장을 보내고 결과를 받으며, 연결 하나에서 여러 문장을 보낼 수 있다. 문장 하나·연결 하나·트랜잭션 하나는 같은 개수 단위가 아니다. 여러 문장을 한 트랜잭션으로 묶을 수 있다는 앞 절의 경계도 유지한다.
문장이 맞는 것과 그 이름이 실제로 존재하는 것은 다르다
구문 분석은 SQL 문장의 구성과 문법이 정해진 형식에 맞는지 확인하고 그 구조를 표현하는 과정이다. 예를 들어 SELECT와 필요한 표현의 배치가 잘못된 문장은 여기서 정상적인 질의 구조가 되지 못한다.
의미 분석은 문장에 적힌 이름과 연산이 실제 표·열·자료형·함수 등의 무엇을 가리키며 어떻게 해석되는지 확인하는 과정이다. 문법상 이름을 적을 자리에 없는열을 써도 문장 모양 자체는 맞을 수 있지만, 실제로 존재하지 않는 열이면 실행 가능한 요청으로 해석할 수 없다. 숫자에 가능한 계산이 모든 문자열에 똑같이 가능한 것도 아니다.
PostgreSQL 18의 원시 구문 분석은 고정 문법으로 구조를 만들고, 뒤의 변환 단계가 시스템에 저장된 객체 정보를 조회해 의미를 붙인다. 따라서 이름을 실제로 해석하는 일을 원시 구문 분석이 이미 전부 끝냈다고 하지 않는다. 권한 확인도 필요하지만 이 문법·의미의 두 구분을 모든 권한 검사의 단일 실행 시점으로 확대하지 않는다.
주문 데이터와 주문표를 설명하는 정보는 다르다
시스템 카탈로그는 표·열·자료형·제약 같은 구조 정보와 DBMS의 관리 정보를 저장하는 체계다. 문구점 주문의 고객번호 값이 업무 데이터라면, 주문이라는 표에 고객번호 열이 있고 그 열이 어떤 자료형인지 설명하는 정보는 카탈로그가 관리하는 정보다. 데이터 사전이라는 표현도 이런 구조·의미를 설명하는 관리 정보 맥락에서 쓰이지만 제품의 실제 저장 위치와 구성을 확인해야 한다.
DBMS는 이 정보를 이용해 앞의 문장에서 무엇을 읽으려는지 판단한다. 이후에 배울 통계 정보는 표가 무엇인지 선언하는 것에 그치지 않고 행 수·분포를 요약해 비용 판단을 돕는다. 정의 정보와 분포의 추정치를 같은 종류의 정확성 보장으로 보지 않는다.
PostgreSQL의 카탈로그는 일반 표 형태로 저장되지만 사용자가 내부 행을 직접 고치는 것이 정상적인 구조 변경 절차는 아니다. 정식 정의·변경 명령과 권한을 사용해야 내부 관계가 함께 유지된다. 이 교재에서는 내부 카탈로그를 수정하는 명령을 실행하지 않는다.
표처럼 보이는 질의를 원래 표의 읽기로 풀 수 있다
일반 뷰(view)는 미리 정한 질의를 이름으로 참조해 표처럼 사용할 수 있게 한 논리적 구조다. 이 문장에서 일반 뷰는 결과를 별도 데이터로 저장해 두는 구조와 구별한다. 예를 들어 확정주문보기가 주문 가운데 확정된 행만 보여 주도록 정의돼 있다고 하자.
질의 재작성은 질의의 의미와 적용 규칙을 지키면서 내부 표현을 다른 질의 표현으로 바꾸는 과정이다. PostgreSQL의 재작성 단계는 뷰 이름으로 들어온 질의를 그 정의에 따른 원래 표의 읽기로 풀어 표현할 수 있다. 문구점의 뷰를 읽는 요청도 무엇이 확정된 주문인지의 조건을 잃어서는 안 된다.
이것은 아직 어떤 인덱스와 결합 방법을 쓸지 고르는 판단과 다르다. 뷰를 썼다고 결과가 언제나 미리 계산되어 있거나, 필요한 권한·조건이 모두 사라지거나, 항상 더 빨라지는 것도 아니다. 제품의 재작성 규칙과 보안 경계를 만족하는 표현만 사용할 수 있다.
무엇을 할지와 어떤 방법으로 할지를 나눈다
논리 계획은 원하는 결과를 얻는 데 필요한 선택·출력·결합 같은 연산의 관계를 표현한 계획이다. 물리 실행계획은 그 연산을 어떤 접근 방법·알고리즘·순서로 수행할지 구체화한 계획이다. 실제 제품이 반드시 이 두 이름의 파일을 따로 저장한다는 뜻은 아니다. 독자가 판단의 수준을 구분하기 위한 관점이다.
| 묻는 수준 | 고객 C1 주문 목록의 예 |
|---|---|
| 논리적 결과 변환 | C1 행을 남기고 필요한 열을 결과 순서로 정해 최대 20행 반환 |
| 물리 방법 A | 원본 표를 순차적으로 읽어 조건에 맞는 행을 모은 뒤 정렬 |
| 물리 방법 B | 고객·주문시각·주문번호 인덱스의 순서를 이용해 필요한 행 접근 |
두 방법이 같은 값을 같은 순서로 반환해야 대안이 된다. 인덱스를 쓰는 후보가 존재한다는 사실과 실제로 가장 싸다는 판단은 다르다. 특히 앞 절에서 배운 출력 열·원본 확인·분포와 쓰기 부담도 버리지 않는다.
표의 모양을 바꾸어도 원래 사실은 지킨다
개념·논리 모델과 정규화: 뜻을 잃지 않고 표를 나눈다에서는 나눈 표에서 원래 사실만 되찾는 무손실성과, 나눈 곳의 검사로 원래 규칙을 보장하는 종속 보존을 구별했다. 이제는 표를 나누는 설계가 아니라 질의의 계산 순서를 바꾸지만, 더 빠르다는 이유로 결과의 사실이나 규칙을 바꿔서는 안 된다는 판단을 가져온다.
행을 남기고 열을 만들고 서로 맞추는 연산
선택(selection)은 조건을 만족하는 행을 남기는 연산이다. 고객 C1의 주문 조건을 적용하면 다른 고객의 주문은 다음 연산으로 넘기지 않는다.
개념·논리 모델과 정규화: 뜻을 잃지 않고 표를 나눈다의 투영은 지정한 열로 관계를 만들며 중복 행을 하나로 보는 집합 연산이었다. SQL의 출력 열 구성은 지정한 열이나 계산식으로 결과 행의 열을 만들며, 별도 중복 제거를 요구하지 않으면 중복을 그대로 둘 수 있는 처리다. 실행계획에서 이를 투영이라고 부르기도 하지만, 관계 이론의 집합 투영과 중복 처리까지 동일하다고 가정하지 않는다.
조인(join)은 둘 이상의 입력에서 결합 조건을 만족하는 행들을 맞추어 결과를 만드는 연산이다. 여기서는 짝이 있는 행을 내보내는 내부 조인부터 생각한다. 가령 어떤 주문 한 건에 항목이 세 개 있다면 주문번호로 결합한 결과에는 그 주문과 각 항목의 짝이 세 행 나올 수 있다. 조인은 언제나 행 수를 줄이는 연산이 아니다.
어느 연산을 먼저 수행할지는 결과의 의미를 지키는 범위에서 바꾼다. SQL의 중복·NULL·요구한 정렬이나 뒤에서 배울 외부 조인의 행 보존 조건을 무시한 변환은 더 빨라도 정답이 아니다.
예상과 실제는 같은 조건에서 비교한다
물리 DB 설계·인덱스·파티션의 물리 설계 검증은 같은 데이터와 업무량에서 정확한 결과와 읽기·쓰기·공간 비용을 함께 비교하는 일이었다. 실행계획의 추정치를 보여 주는 EXPLAIN과 질의를 실제 실행하는 EXPLAIN ANALYZE의 차이를 유지하며, 이제 추정이 만들어지는 이유와 빗나가는 조건을 살핀다.
최적화기는 미래 비용을 추정해 비교한다
비용 기반 질의 최적화는 같은 결과를 내는 후보 실행 방법의 예상 자원 비용을 비교해 실행계획을 선택하는 접근이다. 후보에는 읽을 경로뿐 아니라 결합 순서·결합 방법·정렬·집계의 배치가 포함된다. 질의 최적화기는 후보 실행계획의 비용을 비교해 사용할 계획을 선택하는 DBMS 구성요소다.
예상 비용은 읽을 페이지와 행 수, 장치 접근·연산·메모리 등의 가정에 영향을 받는다. 앞 절의 EXPLAIN에서 배웠듯 비용 숫자를 곧바로 사용자 응답 밀리초라고 읽지 않는다. 같은 100행 결과라도 수많은 행을 조사한 뒤 남겼는지, 적은 페이지에서 바로 찾았는지에 따라 비용이 다르다.
최적이라는 이름도 언제나 가능한 모든 방법을 시험해 전역 최저 시간을 보장한다는 뜻이 아니다. PostgreSQL 18은 결합 수가 큰 경우 탐색에 드는 시간과 메모리를 제한하기 위한 방법을 사용한다. 추정에 사용한 분포가 틀리거나, 캐시·부하가 바뀌거나, 비교한 후보 범위가 제한되면 선택 결과가 실제로 가장 빠르지 않을 수 있다. 그래서 선택된 계획과 실제 관측을 다시 대조한다.
실행기는 계획의 단계마다 필요한 행을 받아 처리한다
실행기(executor)는 선택된 계획의 연산들을 실제 데이터 접근과 계산으로 수행해 결과 행이나 변경 결과를 만드는 구성요소다. PostgreSQL의 대표 실행 흐름에서는 상위 단계가 다음 행을 요구하고 하위 단계가 행 또는 끝을 돌려주는 방식으로 계획의 단계를 연결한다.
예를 들어 위쪽에 정렬이 있고 아래쪽에 조건에 맞는 행을 읽는 단계가 있다면, 정렬은 먼저 정렬할 입력들을 모아야 첫 결과를 낼 수 있다. 반면 이미 맞는 순서의 인덱스 접근을 사용할 수 있다면 별도 정렬 없이 필요한 결과를 내는 후보가 생긴다. 모든 연산이 첫 입력 한 행만 받자마자 결과를 바로 내는 것은 아니다.
결과를 20행으로 제한해도 저장장치에서 20행만 읽는다고 단정할 수 없다. 앞의 조건 검사·결합·정렬을 위해 더 많은 행을 조사할 수 있기 때문이다. 변경문도 대상 행을 찾아 계산한 뒤 실제 변경을 수행하며, 그 과정에 동시성·보임 범위·변경 기록의 책임이 결합된다. 이 관계를 다음의 행 수 추정과 연산 선택부터 구체화한다.
비용 추정과 실제 실행을 대조한다
몇 행이 남을지 알아야 다음 일을 고를 수 있다
질의 카디널리티는 질의 전체나 실행계획의 한 연산이 내보내는 행 수다. 앞 절 관계 모델의 ‘한 고객에 여러 주문’이라는 참여 관계와 구별한다. 여기서는 고객 C1 조건으로 몇 행이 남는지, 조인 뒤 몇 행으로 늘어나는지를 센다. 실제로 세기 전에는 예상 행 수이고, 실행에서 관측하면 실제 행 수다. 서로 다른 값의 개수도 통계에 쓰지만, 그것 자체가 조건에 맞는 행 수는 아니다.
물리 DB 설계·인덱스·파티션에서 선택도는 조건에 맞는 행의 비율이었다. 전체 주문 100만 행 중 C1의 주문이 100행이라면 선택도는 100 ÷ 1,000,000 = 0.0001, 즉 0.01%다. 고객번호 값이 1만 종류라고 해서 모든 고객의 주문이 꼭 100행씩인 것은 아니다. 일부 고객에게 주문이 몰리면 평균으로 계산한 계획이 빗나간다.
질의 계획용 통계는 전체 행 수·값의 분포·서로 다른 값의 개수 등을 요약해 실행 비용 추정에 쓰는 정보다. 자주 나타나는 값과 빈도, 값을 구간별로 요약한 분포는 모든 행을 미리 읽지 않고 크기를 추정하게 한다. PostgreSQL 18의 ANALYZE는 실행계획의 비용 추정에 필요한 데이터 분포 통계를 수집하는 명령이다. 표본에 의한 요약은 갱신 직후에도 추정치다. 명령이 성공했다고 모든 질의의 추정이 정확해지는 것은 아니다.
서로 관련된 조건을 독립이라고 곱하면
문구점의 정식 주문·배송지 규칙과 별개로, 설명용 분석 표본 10,000행에 ‘입력경로’와 ‘행사코드’라는 두 분류 열을 붙였다고 하자. 입력경로가 ‘전화’인 2,000행과 행사코드가 ‘H1’인 2,000행이 이번 표본에서는 정확히 같은 행들이어서 두 조건을 함께 만족하는 실제 행 수도 2,000행이다. 이 표본의 상관관계가 모든 기간에 성립하는 업무 불변식이라는 뜻은 아니며, 확정 주문의 배송지 필수 규칙도 바꾸지 않는다.
두 조건이 독립이라고 가정하면 추정은 10,000 × 0.2 × 0.2 = 400행이다. 실제 2,000행은 그 추정의 5배다. 두 조건이 같은 주문 집합을 가리키는데 서로 다른 80%를 두 번 덜어낸 셈이다. 이 오차가 뒤의 반복 조인 횟수나 정렬 메모리 예상에 전파될 수 있다.
PostgreSQL 18의 확장 통계는 선택한 여러 열의 관계를 함께 수집한다. 다만 ‘통계 객체를 만들었다’와 ‘실제 통계를 수집했다’는 다르며, 수집에는 ANALYZE가 필요하다. 통계 종류마다 지원하는 조건도 다르다. 예컨대 함수 종속 통계가 모든 범위 조건이나 열끼리의 비교까지 해결하는 것은 아니다. 따라서 추정과 실제가 처음 크게 갈라지는 연산을 찾고, 분포 변화·열 간 상관·통계 수집 상태를 대조한 뒤 바꾼다.
표 전체를 훑을까, 위치를 모아 찾아갈까
물리 DB 설계·인덱스·파티션의 인덱스는 필요한 행으로 가는 별도 접근 경로였다. 이제 그 경로를 언제 쓸지 비교한다.
| 실행 접근 | 실제 움직임 | 유리할 수 있는 상황과 비용 |
|---|---|---|
| 순차 스캔 | 표의 페이지를 따라 행을 검사한다 | 많은 행이 필요하면 반복적인 위치 탐색보다 나을 수 있다 |
| 인덱스 스캔 | 인덱스 조건으로 찾은 위치에서 필요한 행을 얻는다 | 적은 행이나 이미 필요한 순서에 가까운 접근에 유리할 수 있으나 표 페이지 접근이 추가된다 |
| 비트맵을 이용한 스캔 | 인덱스에서 얻은 위치들을 모아 표 페이지 단위 접근에 활용한다 | 흩어진 여러 행 접근을 모을 수 있지만 위치 수집과 조건 재검사 비용이 생긴다 |
이는 접근 방법을 읽기 위한 비교다. 비트맵 실행 방법을 영구 저장하는 비트맵 인덱스와 같은 것으로 부르지 않는다. 인덱스만으로 결과를 내는 경우에도 PostgreSQL의 가시성 확인 때문에 표를 추가로 읽을 수 있다는 앞 절의 경계가 유지된다.
‘전체 스캔 발견’만으로 결함이라고 판정하지 않는다. 표가 작거나 결과 비율이 높으면 타당할 수 있다. 반대로 적은 출력 행을 얻기 위해 매우 많은 행을 읽어 버렸다면 조건·분포·접근 경로를 조사한다. 출력 행 수, 검사한 행 수, 읽은 페이지 수는 서로 다른 양이다.
중첩 루프 조인: 바깥 행마다 안쪽을 찾는다
중첩 루프 조인은 한 입력의 각 행에 대해 다른 입력에서 결합 조건을 만족하는 행을 찾는 방법이다. ‘중첩’은 반복 안에 다시 반복이나 탐색이 들어간다는 뜻이다.
이미 확정한 O101·O102의 상품·금액을 바꾸지 않도록, 별도의 가상 주문 O201·O202로 작은 읽기 예제를 보자. 주문은 O201·O202의 2행이고, 주문항목은 (O201, 연필), (O201, 지우개), (O202, 공책)의 3행이다. 주문번호가 같은 행을 결합한다. O201을 잡고 항목을 찾으면 연필·지우개의 2행이 나오며, O202에서는 공책의 1행이 나온다. 결과는 3행이지 2행도, 항상 2 × 3행도 아니다.
안쪽 전체 3행을 매번 비교하는 단순 방법은 2 × 3 = 6회의 쌍 비교를 한다. 안쪽 주문번호 인덱스를 이용하면 각 주문번호에 해당하는 항목을 찾는 탐색으로 바꿀 수 있다. 실제 제품은 저장·재사용 방식에 따라 동작이 더 다양하다. 바깥 입력이 작고 안쪽 탐색이 싸면 유리하지만, 바깥 행 수를 적게 예상했다가 크게 늘면 안쪽 탐색이 반복되어 느려질 수 있다. 작은 쪽을 언제나 바깥에 두라는 무조건 규칙보다 필터 뒤의 행 수와 안쪽 접근 비용을 함께 본다.
같은 위치 후보가 같은 키를 뜻하지는 않는다
배열·연결·스택·큐·트리·그래프·해시의 해시 기반 구조는 키를 계산된 위치 후보에 대응시켜 찾는 비용을 줄였다. 다른 키가 같은 위치 후보를 만드는 충돌이 가능하므로, 위치를 찾은 뒤에도 실제 키가 같은지 확인해야 했다.
해시 조인: 한쪽으로 찾기용 표를 만들고 대조한다
해시 조인은 결합 값을 해시 함수로 분류한 임시 탐색 구조를 만들고 다른 입력의 결합 값으로 그 구조를 찾아 일치하는 행을 결합하는 방법이다. 배열·연결·스택·큐·트리·그래프·해시에서 해시는 입력을 정한 범위의 값에 대응시켰고 서로 다른 입력이 같은 값에 대응할 수 있었다는 점을 가져온다. 같은 위치에 다른 값이 들어갈 수 있으므로 실제 결합 값도 비교한다.
앞 예제의 작은 주문 2행을 주문번호별로 찾는 구조에 넣은 뒤, 항목 3행을 읽으며 O201 또는 O202를 찾는 식이다. 이 구조는 그 실행을 위해 만드는 것이며, 물리 DB 설계·인덱스·파티션에서 저장해 두는 해시 인덱스가 이미 있어야만 해시 조인을 할 수 있는 것은 아니다.
PostgreSQL 18의 일반적인 해시 조인은 해시 가능한 동등 비교를 결합 기준으로 사용한다. ‘주문금액이 기준금액보다 크다’라는 부등호만 있는 결합에 같은 방법을 무조건 적용할 수 없다. 해시 구조를 만드는 초기 비용과 메모리가 필요하고, 크기나 값 쏠림이 예상보다 크면 여러 묶음으로 나누어 임시 저장장치를 쓰는 비용이 생길 수 있다. 한쪽을 작게 만들 수 있는 조건과 실제 메모리·입출력을 함께 판단한다.
정렬 병합 조인: 같은 순서로 온 값을 함께 전진시킨다
정렬 병합 조인은 결합 값 순으로 정돈된 두 입력을 함께 진행시키며 일치하는 값의 행들을 결합하는 방법이다. 입력이 이미 적합한 순서로 나오면 그 순서를 이용하고, 아니라면 먼저 정렬해야 한다.
주문번호 순의 O201·O202와, 같은 순서의 항목 O201·O201·O202를 나란히 읽는다. O201 주문을 만났을 때는 O201 항목 두 개 모두를 내보낸 뒤 O202로 넘어가야 한다. 양쪽 모두 같은 값의 행이 여러 개라면 해당 값에서 가능한 짝을 빠뜨리지 않아야 한다. 한 번 만났으니 양쪽을 무조건 한 행씩만 넘기는 방식은 결과를 잃을 수 있다.
많은 행을 정돈된 흐름으로 처리할 때 유리할 수 있지만, 사전 정렬 비용과 중복 값 처리 비용을 포함해야 한다. 어떤 열에 인덱스가 존재한다는 사실만으로 지금 입력이 그 결합 순서로 나오는 것도 아니다. 중첩 루프·해시·병합은 결과의 정답을 바꾸는 세 종류의 조인이 아니라, 같은 의미의 조인을 구현하는 서로 다른 실행 방법이다.
정렬과 집계는 출력이 작아도 입력 처리 작업이 필요하다
고객별 주문금액 합계를 구하는 집계는 여러 입력 행의 값을 정한 그룹이나 전체 범위별로 합계·개수 등의 요약값으로 만드는 연산이다. SQL의 GROUP BY는 어떤 값이 같은 행을 한 그룹으로 볼지 지정한다. 주문항목을 주문번호로 묶으면 O101의 항목 두 개가 한 주문 합계에 기여한다.
집계 결과가 고객 100명의 100행이라고 해서 원주문을 100행만 읽은 것은 아니다. 그룹을 정렬해 연속으로 처리하거나, 그룹 값별 임시 탐색 구조에 합계를 모으는 방법 등이 가능하다. 정렬도 입력량이 메모리 범위를 넘으면 일부를 임시 저장장치에 놓고 다시 합치는 비용이 생긴다.
PostgreSQL의 work_mem은 정렬·해시 같은 실행 작업이 임시 파일 사용을 판단하는 데 쓰는 기본 메모리 한도 설정이다. 서버 전체나 질의 한 개의 총메모리 상한과 같지 않다. 여러 실행 연산·세션·병렬 작업이 동시에 사용하면 전체 요구가 커지고, 해시 작업에는 별도 배수 설정도 작용한다. 작업 하나의 임시 파일을 없애려고 값을 크게 올렸더니 동시 부하에서 메모리가 부족해지는 상황도 고려한다. 수치를 외우기보다 동시 작업 수, 행 폭, 그룹 수, 임시 입출력을 확인한다.
일을 줄이는 변환도 먼저 결과의 뜻을 지켜야 한다
고객 C1의 주문만 필요한 내부 조인에서는 가능한 경우 그 조건을 앞에서 적용해 조인 입력을 줄일 수 있다. 그러나 ‘필터를 전부 앞으로 옮기면 된다’는 규칙은 틀리다.
주문이 없는 고객 C3도 보여 주어야 한다면 왼쪽 외부 조인은 왼쪽 입력에 짝이 없는 행도 보존하고 오른쪽 값을 NULL로 채워 내보내는 결합이다. C3의 주문 값은 NULL이 된다. 조인한 뒤 ‘오른쪽 주문금액이 0보다 크다’라는 WHERE 조건을 적용하면 C3는 참으로 판정되지 않아 빠진다. 그 조건을 ON의 결합 조건에 두고 왼쪽 행 보존을 유지하는 경우와 결과가 다르다. NULL은 앞 절처럼 숫자 0이나 빈 문자열이 아니며, WHERE는 참인 행만 남긴다.
또한 중복 제거를 몰래 추가하면 같은 값의 두 주문항목을 하나로 잃을 수 있다. 필요한 열을 일찍 줄이더라도 뒤의 결합·조건·정렬에 필요한 열은 남겨야 한다. 조건 이동·조인 순서 변경·열 축소는 NULL, 중복, 외부 행 보존과 요구한 순서를 지키는 범위에서만 성능 개선이다. 결과 건수가 같다는 사실만으로 값과 중복까지 같은 결과라고 판단하지 않는다.
외부 조인은 ‘짝 찾기’와 ‘결과 걸러내기’를 나누어 추적한다. ON은 어떤 오른쪽 행을 짝으로 인정할지 정한다. 그 조건에 맞는 짝이 없는 왼쪽 행에는 오른쪽 열을 NULL로 채운 행을 만들어 준다. WHERE는 이렇게 만들어진 결과에서 참인 행만 남긴다. NULL과의 > 0 비교는 참이 아니므로 그 행을 남기지 않는다. 따라서 양수 주문만 짝으로 인정하면서 모든 고객을 남기려는 요구와, 양수 주문이 있는 고객만 결과에 남기려는 요구는 다르다. 짝을 못 찾은 NULL은 금액 0인 주문과도 구별해야 한다.
실행계획 숫자는 단위를 맞추어 읽는다
물리 DB 설계·인덱스·파티션에서 EXPLAIN은 계획을 보고, EXPLAIN ANALYZE는 실제 실행까지 한다고 배웠다. 여기의 숫자는 설명용이며 실행 결과를 꾸며 낸 실측값이 아니다. 운영 변경문을 ‘분석만 한다’고 실행해서는 안 된다.
가상 출력의 cost=5..40 rows=4는 시작 비용 5, 끝까지 실행할 때의 누적 비용 40, 출력 예상 4행이라는 뜻으로 읽는다. 비용 40을 40ms로 환산하지 않는다. 실제 계측에서 안쪽 연산이 actual rows=30 loops=10이라면, PostgreSQL의 행 수는 한 실행당 평균이므로 전체 출력은 30 × 10 = 300행이다. rows=4와 비교할 때는 한 번의 예상 4행 대 평균 실제 30행을 먼저 대조하고, 반복 10회의 전체 영향도 본다.
상위 연산 비용에는 하위 연산 비용이 포함되므로 계획 각 줄을 모두 더하면 중복 계산한다. 실제 시간도 포함 관계와 병렬 실행을 확인해야 한다. 추정/실제 행 수의 차이, 반복 수, 버퍼 접근, 임시 입출력, 잠금 대기를 나누고, 같은 데이터·조건·동시 부하에서 응답시간과 처리 결과를 함께 대조한다. 인덱스나 메모리 설정 변경은 이 원인 확인 뒤의 후보이며, 더 낮은 추정 비용만으로 성공을 선언하지 않는다.
다른 출력 읽기. 같은 종류의 안쪽 연산이 rows=4, actual rows=8 loops=50으로 바뀌었다면 한 번의 추정 오차와 전체 출력은 각각 얼마인가?
풀이를 열기 전에 한 번당 오차와 전체 반복 처리량을 각각 계산하고, 두 값의 단위가 어떻게 다른지 말해 본다.
풀이 방향과 완성 답안 보기
해설은 8÷4=2배, 8×50=400행이다. 400÷4=100배는 전체 실제량을 한 번의 추정과 비교해 단위를 섞은 답이다. 또한 400행은 반복 출력의 합이므로 서로 다른 주문이 반드시 400건이라는 뜻은 아니다. 위 설명으로 돌아가 한 번당 행 수와 반복 횟수를 먼저 나누어 적으면 교정할 수 있다.
실행계획 숫자의 단위 설명으로 돌아가 추정과 실제의 비교 단위를 다시 확인한다.
버퍼는 페이지를 다시 읽는 비용을 줄이는 자리다
DB 버퍼 풀은 저장장치의 DB 페이지를 메모리에 두고 읽기와 변경에 재사용하는 관리 영역이다. 앞 절의 DB 페이지는 파일의 고정 크기 저장 단위였고, 여기서는 그 페이지의 메모리 쪽 사본과 관리 상태를 본다. 필요한 페이지가 있으면 메모리에서 이용하고, 없으면 하위 저장 경로에서 읽어 넣는다. 운영체제에도 별도 캐시가 있을 수 있으므로 DB 버퍼에 없었다는 사실이 곧 장치가 실제로 읽었다는 뜻은 아니다.
더티 페이지는 메모리에서 변경되어 그 변경이 아직 해당 데이터 파일에 모두 반영되지 않은 DB 페이지다. ‘더티’는 자료가 틀리다는 말이 아니다. 또 커밋 여부와도 다른 축이다. 확정한 변경이 더티 페이지에 남아 있을 수 있고, 아직 확정하지 않은 작업이 페이지를 변경할 수도 있다. 이 상태와 장애 복구의 연결은 뒤에서 복구 로그의 기록 순서로 설명한다.
버퍼가 유한하므로 새 페이지를 들이려면 다른 페이지를 내보낼 자리를 골라야 한다. 현재 사용 중인 페이지를 함부로 교체하지 않도록 보호하고, 바뀐 페이지는 안전한 기록 순서를 지켜 저장한다. 적중률이 높아도 같은 페이지를 불필요하게 수십만 번 찾거나 잠금을 기다리면 느릴 수 있다. 메모리 증설만으로 비효율적인 행 처리와 대기 원인이 모두 없어지지는 않는다.
동시 실행에서도 주문 규칙을 지킨다
읽기가 빨라도 함께 바꾸면 규칙이 깨질 수 있다
트랜잭션의 네 성질(ACID)의 ACID는 전부 성공·규칙 보존·동시 간섭 통제·확정 결과의 지속이라는 트랜잭션 경계의 보장이었다. 동기화·경쟁·교착상태에서 잠금은 공유 상태의 접근을 조정하지만, 서로 가진 자원을 기다리는 교착상태를 만들 수도 있었다. 여기서는 그 원리를 DB의 행·조건 집합과 트랜잭션 완료에 적용하며, 일반 동기화 도구의 정의를 다시 만들지 않는다.
직렬 실행과 직렬가능성은 같지 않다
트랜잭션 스케줄은 여러 트랜잭션의 읽기·쓰기·완료 동작이 어떤 순서로 실행되는지를 표현한 것이다. 각 트랜잭션 내부에서 정한 순서는 지키되 서로의 동작이 끼어들 수 있다.
직렬 실행은 한 트랜잭션을 끝낸 뒤 다음 트랜잭션을 시작해 서로의 동작을 섞지 않는 실행이다. 직렬가능성은 함께 실행한 트랜잭션들의 효과가 어떤 직렬 실행의 효과와 같도록 하는 성질이다. 실제로 하나씩만 실행하라는 요구가 아니라, 함께 실행하더라도 설명 가능한 하나씩의 순서가 있어야 한다는 뜻이다. 문자·인코딩·직렬화·엔디언의 데이터를 바이트로 바꾸는 ‘직렬화’와는 다른 개념이다.
문구점의 재고 1개를 확인하고 주문하는 두 작업에서 첫 작업이 재고를 0으로 만들었다면, 어떤 직렬 순서에서도 뒤 작업은 재고 부족을 보아야 한다. 두 작업이 동시에 ‘재고 1’을 근거로 각자 주문을 확정했다면 둘 다 성공한 결과는 그 규칙을 지키는 직렬 실행으로 설명되지 않는다. 다만 직렬가능성은 각 트랜잭션 자체의 업무 규칙이 맞다는 전제를 대신하지 않는다. 재고 검사를 누락한 잘못된 트랜잭션을 하나씩 실행해도 잘못된 주문은 생긴다.
충돌 그래프는 읽고 쓴 순서의 모순을 찾는다
여기서는 한 값에 하나의 현재 버전만 두는 단순한 읽기·쓰기 모형을 사용한다. 충돌 직렬가능성은 서로 다른 트랜잭션의 충돌하는 동작 순서를 유지한 채 어떤 직렬 실행과 동등하게 설명할 수 있는 성질이다. 같은 항목을 다루고 적어도 하나가 쓰기인 읽기–쓰기, 쓰기–읽기, 쓰기–쓰기의 순서가 충돌 판단 대상이다. 읽기–읽기만으로는 이 모형의 충돌이 아니다.
가상 재고 항목 X와 Y에서 다음 동작들이 모두 수행되고 두 작업이 확정됐다고 하자. R1(X)는 T1의 X 읽기, W2(X)는 T2의 X 쓰기라는 표기다.
R1(X) → W2(X) → R2(Y) → W1(Y) → 두 작업의 커밋X는 T1이 T2보다 먼저, Y는 T2가 T1보다 먼저여야 한다. T1→T2→T1 순환은 충돌 직렬가능성의 모순이며 현재 잠금 대기를 뜻하지 않는다.
선행 그래프는 트랜잭션을 점으로 두고 충돌 때문에 먼저 와야 하는 순서를 화살표로 나타낸 것이다. 이 모형에서 그래프가 순환하지 않으면 충돌 순서를 만족하는 직렬 순서를 만들 수 있고, 순환하면 충돌 직렬가능하지 않다. 예제는 T1이 T2보다 앞이면서 뒤여야 하므로 모순이다. 이는 잠금을 기다리는 상태를 그린 그래프가 아니며, 실제로 대기 없이 잘못된 값으로 완료된 스케줄에도 적용한다.
뷰 직렬가능성은 각 읽기가 어느 쓰기에서 값을 받는지와 각 항목의 마지막 쓰기를 유지하는 어떤 직렬 실행이 존재하는 성질이다. 처음 값을 읽는 관계도 유지한다. 이 기준은 충돌 직렬가능성보다 넓으므로 둘을 무조건 같다고 부르지 않는다. 여러 버전을 읽거나 조건에 맞는 행 집합이 변하는 구현에서는 실제 읽은 버전·조건에 대한 의존성까지 반영해야 하므로, 이 네 동작짜리 단일 버전 그림만으로 모든 제품의 격리를 판정하지 않는다.
완료 순서도 안전해야 한다
회복 가능한 스케줄은 다른 트랜잭션이 쓴 값을 읽은 작업이 그 값을 쓴 작업의 커밋보다 먼저 커밋하지 않는 스케줄이다. 아직 확정되지 않은 T1의 할인금액을 T2가 읽었다면, T1이 취소될 가능성이 남은 동안 T2만 완료됐다고 보장해서는 안 된다.
T1이 임시로 할인금액을 5,000원으로 바꾸고 T2가 그 값을 읽어 주문 합계를 계산했다고 하자. T1이 롤백되면 T2도 그 값에 의존한 처리를 취소해야 한다. 다른 작업의 취소 때문에 의존 작업의 취소가 이어지는 것을 연쇄 롤백이라고 한다. 애초에 다른 트랜잭션의 미확정 값을 읽지 않게 하면 이러한 읽기 의존에 따른 연쇄 취소를 피할 수 있다.
따라서 ‘직렬 순서와 같은 효과’와 ‘실패한 작업을 안전하게 취소할 수 있음’은 구별해 점검한다. 엄격한 실행은 다른 트랜잭션이 쓴 항목을 그 트랜잭션이 끝나기 전에 읽거나 덮어쓰지 못하게 하는 실행이다. 이는 취소의 복잡성을 줄인다. 구체적으로 잠금을 언제 풀지, 버전 중 무엇을 보일지, 실패 뒤 무엇을 다시 적용할지는 이 보장을 구현하는 서로 다른 장치다.
공유 잠금과 배타 잠금은 같은 대상의 허용 조합이다
DB 공유 잠금은 정한 데이터 대상의 읽기를 여러 트랜잭션에 함께 허용하되 충돌하는 변경을 막는 잠금 모드다. DB 배타 잠금은 정한 데이터 대상에 다른 트랜잭션의 충돌하는 읽기·쓰기 잠금을 함께 허용하지 않는 잠금 모드다. 전통적인 두 모드 설명에서는 공유를 S(shared), 배타를 X(exclusive)로 적는다.
| 같은 대상의 기존 잠금 | 새 S 요청 | 새 X 요청 |
|---|---|---|
| S | 함께 허용 | 대기 등 충돌 처리 |
| X | 대기 등 충돌 처리 | 대기 등 충돌 처리 |
이 작은 표는 기본 S/X 모형의 호환성이다. 다른 행을 잠그는 요청까지 무조건 충돌하지 않으며, 제품의 모든 잠금 모드를 이 표 두 칸으로 대신할 수는 없다. 특히 PostgreSQL의 일반적인 버전 읽기는 행의 쓰기 잠금과 같은 방식으로 충돌하지 않는다. ‘행을 쓰는 동안 누구도 그 행을 읽을 수 없다’고 일반화하지 않는다.
문구점의 특정 재고 행을 변경 전에 SELECT … FOR UPDATE로 잠그는 경우에는 같은 행을 바꾸거나 호환되지 않는 잠금을 얻으려는 다른 작업이 기다릴 수 있다. 잠금은 획득한 순간부터 존재하므로, 오래된 값을 이미 읽고 판단한 뒤 쓰기만 잠그는 것으로 앞의 판단까지 안전해지지는 않는다. 조건 확인과 변경을 같은 보호 경계에 놓아야 한다.
행·표·범위의 잠금은 보호 범위가 다르다
잠금 단위는 하나의 잠금이 보호하는 행·페이지·표 등 데이터 범위다. 행 단위는 무관한 행의 병행 처리를 허용하기 쉽지만 관리할 잠금 수가 늘 수 있다. 표 전체는 관리 항목을 줄일 수 있지만 영향받는 작업 범위가 커진다. 작은 잠금이라고 언제나 전체 성능이 좋거나 큰 잠금이라고 항상 틀린 것은 아니다.
의도 잠금은 더 작은 하위 대상에 잠금을 사용할 의사를 상위 대상에 표시하여 서로 다른 단위의 잠금 충돌을 조정하는 모드다. 예를 들어 행에 배타 잠금을 쓰려는 작업이 표 수준에 의도를 표시하면, 다른 작업의 표 전체 읽기·쓰기 잠금 요청이 그 하위 변경을 놓치지 않을 수 있다. 의도 공유(Intention Shared, IS)는 하위 대상에 공유 잠금을 사용하려는 의도를 나타내는 모드다. 의도 배타(Intention Exclusive, IX)는 하위 대상에 배타 잠금을 사용할 수 있다는 의도를 나타내는 모드다. 표 모든 행을 이미 공유·배타로 잠갔다는 뜻은 아니다. 이 다중 단위 원리와 PostgreSQL의 ROW SHARE, ROW EXCLUSIVE라는 이름을 기계적으로 일대일 대응시키지 않는다. PostgreSQL의 두 이름은 모두 표 수준 모드다.
존재하는 행만 보호해서는 ‘조건에 맞는 새 행이 생기면 안 된다’는 요구를 놓칠 수 있다. 예컨대 어떤 금액 구간의 주문 집합을 안정되게 판단하려면 제품·격리 방식에 따라 범위나 조건에 대한 보호도 필요하다. 잠금 이름뿐 아니라 대상, 모드, 보유 기간, 호환성, 새 행 삽입에 대한 효과까지 확인한다.
2단계 잠금은 해제 뒤 새 잠금을 얻지 않는 규칙이다
2단계 잠금(Two-Phase Locking, 2PL)은 잠금을 얻는 확장 단계와 놓는 축소 단계를 구분하여, 한 번 잠금을 놓은 뒤에는 새 잠금을 얻지 않는 프로토콜이다. 적절한 잠금으로 모든 충돌을 다루는 단일 버전 모형에서 충돌 직렬가능성을 보장하는 방법이다.
첫 잠금 해제 뒤에는 새 잠금을 얻지 않는다 · 상태 따라가기
기본 2PL 사례에서 X를 얻고 Y까지 얻어 처리한다. 모든 단계에서 보유 잠금·2PL 단계의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
확장 단계에서 X·Y의 잠금을 얻어 처리한다. 처음 잠금을 놓으면 축소 단계가 시작되어 X·Y를 해제하며 새 잠금을 얻을 수 없다. 기본 2PL의 해제 시점이 커밋 뒤라는 뜻은 아니다.
이 예의 조건
- 확장 단계에서 X·Y의 잠금을 얻어 처리한다. 처음 잠금을 놓으면 축소 단계가 시작되어 X·Y를 해제하며 새 잠금을 얻을 수 없다. 기본 2PL의 해제 시점이 커밋 뒤라는 뜻은 아니다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 대상 | 원 설명 |
|---|---|
| X 잠금 획득 | X 처리 |
| Y 잠금 획득 | Y 처리 |
| X 잠금 해제 | 축소 단계 시작 이후 새 획득 금지 |
| Y 잠금 해제 | 남은 잠금 해제 |
| 연결 | 원 라벨·방향 |
|---|---|
| X 잠금 획득 → Y 잠금 획득 | 라벨 없는 연결 / 정방향 / 실선 |
| Y 잠금 획득 → X 잠금 해제 | 처음 해제 / 정방향 / 실선 |
| X 잠금 해제 → Y 잠금 해제 | 라벨 없는 연결 / 정방향 / 실선 |
| 경계 | 포함 대상 |
|---|---|
| 확장: 획득 가능 | X 잠금 획득, Y 잠금 획득 |
| 축소: 해제만 가능 | X 잠금 해제, Y 잠금 해제 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 성장과 두 잠금 기본 2PL 사례에서 X를 얻고 Y까지 얻어 처리한다. |
|
|
| X 첫 해제 X를 처음 해제하면 보유 잠금은 Y만 남고 수축 단계가 시작된다. |
|
|
| Y 해제 완료 남은 Y를 해제하면 보유 잠금이 없어진다. |
|
|
첫 잠금 해제 뒤에는 새 잠금을 얻지 않는다 · 상태 따라가기
기본 2PL 사례에서 X를 얻고 Y까지 얻어 처리한다. 모든 단계에서 보유 잠금·2PL 단계의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
확장 단계에서 X·Y의 잠금을 얻어 처리한다. 처음 잠금을 놓으면 축소 단계가 시작되어 X·Y를 해제하며 새 잠금을 얻을 수 없다. 기본 2PL의 해제 시점이 커밋 뒤라는 뜻은 아니다.
이 예의 조건
- 확장 단계에서 X·Y의 잠금을 얻어 처리한다. 처음 잠금을 놓으면 축소 단계가 시작되어 X·Y를 해제하며 새 잠금을 얻을 수 없다. 기본 2PL의 해제 시점이 커밋 뒤라는 뜻은 아니다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 대상 | 원 설명 |
|---|---|
| X 잠금 획득 | X 처리 |
| Y 잠금 획득 | Y 처리 |
| X 잠금 해제 | 축소 단계 시작 이후 새 획득 금지 |
| Y 잠금 해제 | 남은 잠금 해제 |
| 연결 | 원 라벨·방향 |
|---|---|
| X 잠금 획득 → Y 잠금 획득 | 라벨 없는 연결 / 정방향 / 실선 |
| Y 잠금 획득 → X 잠금 해제 | 처음 해제 / 정방향 / 실선 |
| X 잠금 해제 → Y 잠금 해제 | 라벨 없는 연결 / 정방향 / 실선 |
| 경계 | 포함 대상 |
|---|---|
| 확장: 획득 가능 | X 잠금 획득, Y 잠금 획득 |
| 축소: 해제만 가능 | X 잠금 해제, Y 잠금 해제 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 성장과 두 잠금 기본 2PL 사례에서 X를 얻고 Y까지 얻어 처리한다. |
|
|
| X 첫 해제 X를 처음 해제하면 보유 잠금은 Y만 남고 수축 단계가 시작된다. |
|
|
| Y 해제 완료 남은 Y를 해제하면 보유 잠금이 없어진다. |
|
|
한 항목을 읽고 잠금을 풀어 버린 뒤 다른 항목의 잠금을 새로 얻으면, 그 사이에 다른 작업이 들어와 항목별 선후 관계를 뒤집을 수 있다. 반면 2PL을 지켜도 서로 필요한 잠금을 보유한 채 기다리는 교착상태는 생길 수 있다. 직렬가능성 보장과 교착 없음은 같은 보장이 아니다.
기본 2PL은 해제 시점을 반드시 커밋 뒤로 고정하지 않는다. 미확정 쓰기를 다른 작업에 노출하지 않으려면 배타 잠금을 커밋·롤백 완료까지 유지하는 엄격한 규칙을 추가한다. 엄격한 2PL(Strict 2PL)은 이 절에서 최소한 배타 잠금을 트랜잭션 종료까지 유지하여 미확정 쓰기를 보호하는 2PL 변형을 가리킨다. 문헌마다 ‘strict’의 용례가 다르므로 답안에는 이름만 쓰지 않고 배타 잠금만 끝까지인지, 공유 잠금도 끝까지인지를 적는다. 여기서 읽은 Bernstein 등의 원전은 모든 잠금을 완료 시 함께 해제하는 변형을 Strict 2PL이라고 부르며, 쓰기 잠금의 종료까지 보유가 엄격성에 핵심임도 설명한다.
보수적 2PL(Conservative 2PL)은 필요한 잠금 집합 전체를 미리 확보할 수 있을 때에만 데이터 처리를 시작하는 변형이다. 하나라도 충돌하면 일부를 보유한 채 기다리지 않고, 필요한 잠금을 모두 얻을 수 있을 때까지 아무 잠금도 갖지 않은 채 기다린다. 잠금 집합을 미리 알아야 하고 불필요한 대기가 늘 수 있다. 2PL의 ‘두 단계’는 잠금 획득과 해제 규칙이며, 여러 서버의 완료 합의를 다루는 2단계 커밋과는 목적이 다르다.
교착은 기다림의 고리를 끊고 전체 판단을 다시 한다
동기화·경쟁·교착상태의 교착상태를 문구점 DB에 적용해 보자. T1은 연필 재고 행을, T2는 공책 재고 행을 각각 변경용으로 잠갔다. T1이 공책 행을 기다리고 T2가 연필 행을 기다리면 아무도 먼저 완료하지 못한다. 한쪽이 단순히 오래 걸리는 것과 서로를 기다리는 것은 구별해야 한다.
서로 보유한 잠금을 기다려 완료하지 못한다 · 상태 따라가기
T1은 연필, T2는 공책을 각각 보유한다. 모든 단계에서 T1·연필·T2·공책의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다.
이 예의 조건
- T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다.
- 서로 보유한 잠금을 기다려 완료하지 못한다. T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다.
- T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고 T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 실제 두 노드와 두 화살표 모두 현재 대기를 나타내며 점선은 역방향 순환을 강조한다. 각 노드에는 들어오는 관계가 하나다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 원 항목 | 설명·조건 |
|---|---|
| 11 | 서로 보유한 잠금을 기다려 완료하지 못한다. T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 서로 보유 T1은 연필, T2는 공책을 각각 보유한다. |
|
|
| T1 공책 대기 T1은 T2가 보유한 공책을 기다린다. |
|
|
| T2 연필 대기 T2도 T1의 연필을 기다려 현재 대기가 고리를 만든다. |
|
|
서로 보유한 잠금을 기다려 완료하지 못한다 · 상태 따라가기
T1은 연필, T2는 공책을 각각 보유한다. 모든 단계에서 T1·연필·T2·공책의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다.
이 예의 조건
- T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다.
- 서로 보유한 잠금을 기다려 완료하지 못한다. T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다.
- T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고 T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 실제 두 노드와 두 화살표 모두 현재 대기를 나타내며 점선은 역방향 순환을 강조한다. 각 노드에는 들어오는 관계가 하나다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 원 항목 | 설명·조건 |
|---|---|
| 11 | 서로 보유한 잠금을 기다려 완료하지 못한다. T1은 연필 잠금을 보유한 채 T2의 공책 잠금을 기다리고, T2는 공책 잠금을 보유한 채 T1의 연필 잠금을 기다린다. 두 화살표 모두 현재 대기이며 점선은 순환의 역방향만 강조한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 서로 보유 T1은 연필, T2는 공책을 각각 보유한다. |
|
|
| T1 공책 대기 T1은 T2가 보유한 공책을 기다린다. |
|
|
| T2 연필 대기 T2도 T1의 연필을 기다려 현재 대기가 고리를 만든다. |
|
|
동기화·경쟁·교착상태에서 배운 대기 그래프의 ‘다른 작업이 보유한 자원을 기다림’이라는 화살표를 여기서는 DB의 잠금 대기에 적용한다. 앞의 선행 그래프가 ‘완료된 실행을 어떤 순서로 설명할까’를 묻는 것과 달리, 이 그림은 현재 ‘누가 누구를 기다리나’를 묻는다. PostgreSQL은 교착을 발견하면 관련 트랜잭션 중 하나를 중단시켜 진행하게 하므로 애플리케이션의 오류 처리가 필요하다.
여러 행은 모든 관련 처리 경로에서 같은 순서로 잠그고, 필요한 보호 범위는 유지하면서 트랜잭션을 짧게 하며, 사용자 입력을 기다리는 동안 잠금을 잡아 두지 않는다. 교착이나 직렬성 충돌로 중단됐다면 맨 마지막 쓰기만 반복하지 말고 새로운 트랜잭션에서 읽기·조건 판단·변경을 다시 한다. 외부 결제까지 무조건 재실행해도 된다는 뜻은 아니며, 앞에서 배운 주문 식별자와 외부 처리의 중복 방지 경계를 유지한다. 대기 제한시간 만료는 교착의 증명도, 작업이 전혀 수행되지 않았다는 증명도 아니다.
무엇을 보았는지로 격리 이상을 구별한다
더티 리드(dirty read)는 다른 트랜잭션이 아직 커밋하지 않은 변경을 읽는 현상이다. T1이 임시로 바꾼 재고 0을 T2가 읽었는데 T1이 취소되면, T2는 끝내 확정되지 않은 변경을 근거로 판단한 셈이다. 더티 페이지가 메모리와 파일 사이 반영 상태였다면, 더티 리드는 다른 작업의 미확정 값을 보았는가에 관한 문제다.
비반복 읽기(nonrepeatable read)는 같은 트랜잭션이 이전에 읽은 데이터를 다시 읽었을 때 다른 트랜잭션의 커밋된 변경 때문에 값이 달라지는 현상이다. 재고 1을 읽은 뒤 다른 작업이 0으로 바꾸어 확정하여 재조회가 0이 되는 경우다. 미확정 값을 읽은 것은 아니지만, 두 조회가 하나의 같은 기준 상태를 보지는 않았다.
팬텀 읽기(phantom read)는 같은 트랜잭션이 같은 검색 조건으로 다시 조회했을 때 다른 트랜잭션의 커밋 때문에 조건을 만족하는 행 집합이 달라지는 현상이다. ‘오늘 확정한 주문’을 두 번 조회하는 사이 새 주문이 확정되어 하나 더 보이는 경우다. 새 행 삽입뿐 아니라 수정·삭제로 조건 충족 집합이 바뀌는 경우도 생각해야 한다.
어떤 현상을 허용할지는 업무에 따라 다르다. 새로고침할 때 최신 값이 바뀌는 화면에는 자연스러운 동작도, 여러 조회를 합쳐 하나의 보고서를 만드는 트랜잭션에는 문제가 될 수 있다. 세 현상을 막았다는 것만으로 모든 직렬성 이상이 사라졌다고 결론 내리지는 않는다. 서로 다른 행을 바꾸면서 전체 규칙을 깨는 사례를 조금 뒤에 읽는다.
PostgreSQL 18의 가시성과 충돌 처리를 읽는다
MVCC는 읽을 수 있는 버전을 골라 준다
다중 버전 동시성 제어(Multiversion Concurrency Control, MVCC)는 데이터의 여러 버전과 트랜잭션 상태를 이용해 각 작업에 보일 버전을 판단하는 동시성 제어 방식이다. DB 읽기 스냅샷은 특정 읽기에서 어떤 트랜잭션의 변경을 볼 수 있는지 정하는 기준 상태다. 모든 데이터를 매번 별도 파일로 복사한 백업이 아니며, 저장장치 스냅샷과도 구별한다.
설명용 재고가 1일 때 읽기 T1의 기준 상태가 잡혔다고 하자. T2가 이를 0으로 바꾸는 동안 T1은 자기 기준에서 보이는 이전 버전 1을 읽을 수 있다. T2가 커밋한 뒤의 새로운 기준으로 시작한 읽기는 0을 볼 수 있다. 같은 행에 대해 서로 다른 값을 읽더라도 각자의 격리 계약에 맞는 버전이면 그것만으로 오염은 아니다.
PostgreSQL에서는 행의 생성·변경 관련 트랜잭션 정보와 완료 상태가 보일 버전 판단에 쓰인다. 자기 트랜잭션에서 앞서 변경한 값도 고려한다. MVCC는 읽기와 쓰기의 간섭을 줄이는 구현 수단이지, 항상 최신 값·모든 트랜잭션에 동일한 시점·완전한 직렬가능성을 자동 보장하는 격리 수준 이름이 아니다. 일반 읽기가 행 쓰기와 충돌하지 않더라도 같은 행을 바꾸는 작업, 명시적 잠금, 표 구조 변경 등에 따른 대기는 여전히 존재한다.
Read Committed에서는 일반 조회마다 기준이 새로 생긴다
Read Committed는 다른 트랜잭션의 미확정 변경을 읽지 않도록 하는 격리 수준이다. PostgreSQL 18의 기본 수준이며, 일반 SELECT는 그 문장이 시작할 때의 스냅샷을 사용한다. 트랜잭션 하나에서 조회를 두 번 해도 그 사이 다른 작업의 커밋이 두 번째 조회에 반영될 수 있다. 이는 이 수준의 계약이지 항상 결함은 아니다.
PostgreSQL 18의 UPDATE는 대상 행을 찾은 뒤 다른 변경 작업을 기다릴 수 있고, 앞 작업이 커밋하면 바뀐 행에 조건을 다시 검사한다. 따라서 일반 조회가 같은 문장 중의 다른 커밋을 안 본다는 설명을 ‘모든 변경문이 언제나 완전히 같은 상태만 본다’로 확장하면 안 된다. 여러 행을 복잡한 조건으로 판단하는 업무에서는 이 차이가 중요하다.
Read Uncommitted는 SQL 격리 구분에서 다른 트랜잭션의 미확정 변경 읽기를 허용할 수 있는 수준이다. 그러나 PostgreSQL 18에서 이 이름을 요청해도 동작은 Read Committed와 같아 더티 리드를 허용하지 않는다. 제품이 표준의 최소 보장보다 강하게 동작할 수 있으므로, 격리 수준의 이름과 실제 버전별 동작을 함께 적는다.
Repeatable Read의 이름만으로 모든 제품을 같게 읽지 않는다
Repeatable Read는 SQL 격리 구분에서 다른 트랜잭션의 변경으로 이미 읽은 값이 달라지는 비반복 읽기를 허용하지 않는 수준이다. 스냅샷 격리(Snapshot Isolation, SI)는 트랜잭션이 정해진 기준의 버전을 읽고 동시 쓰기 충돌을 통제하는 격리 방식이다. PostgreSQL 18의 Repeatable Read는 SI로 구현되어 일반 SQL 격리 구분보다 강하게 팬텀 읽기도 막지만, 모든 직렬성 이상까지 막지는 않는다.
PostgreSQL 18에서는 트랜잭션 제어 문장이 아닌 첫 문장이 시작할 때 기준이 잡힌다. 단순히 BEGIN이라는 시작 명령을 보낸 벽시계 시각과 언제나 같다고 외우지 않는다. 이후 자기 트랜잭션의 변경은 읽을 수 있지만 다른 트랜잭션이 뒤에 커밋한 변경으로 조회 기준을 계속 바꾸지는 않는다. 기준 뒤 바뀐 행을 갱신하려는 등 충돌이 생기면 오류로 중단될 수 있어 전체 트랜잭션 재시도를 준비한다.
Serializable은 동시 실행의 효과를 어떤 직렬 실행과 같게 보장하려는 격리 수준이다.
| PostgreSQL 18에서 고른 수준 | 일반 읽기의 기준 | 여전히 점검할 경계 |
|---|---|---|
| Read Uncommitted / Read Committed | 문장마다 새 스냅샷 | 비반복 읽기·팬텀·업무 규칙 간섭 |
| Repeatable Read | 트랜잭션의 기준 스냅샷 | 서로 다른 행의 변경이 만드는 직렬성 이상·재시도 |
| Serializable | 기준 스냅샷에 직렬성 충돌 검사 추가 | 중단·재시도 비용과 트랜잭션 자체의 업무 오류 |
마지막 수준은 앞에서 배운 직렬가능성을 제공하려는 계약이며, 다음 사례에서 왜 추가 검사가 필요한지 확인한다. 다른 제품의 Repeatable Read 또는 Serializable을 PostgreSQL 18의 이 표로 대체하지 않는다.
각자 다른 행을 썼는데 할인 예산을 함께 넘었다
쓰기 편향(write skew)은 같은 기준의 데이터를 읽은 동시 트랜잭션들이 서로 다른 행을 변경하여, 각자의 변경만 보면 지켜지는 듯했던 전체 규칙을 깨뜨리는 이상 현상이다. 단순한 ‘같은 행의 마지막 쓰기 덮어쓰기’와 구별한다.
기존 O101·O102 거래를 바꾸지 않는 별도 설명 상황이다. 문구점의 하루 추가 할인 예산이 10만 원이고 현재 사용액은 0원이라고 하자. T1과 T2는 서로 다른 새 주문에 6만 원씩 할인을 승인하려 한다. 각자 ‘현재 사용액 + 내 할인액 ≤ 10만 원’을 확인한 뒤 자기 주문의 할인 승인 행을 추가한다.
같은 기준을 읽고 다른 행을 쓰면 합계가 넘을 수 있다 · 상태 따라가기
한도 100k·현재 합계 0을 두 거래가 읽는다. 모든 단계에서 T1·T2 읽기·행·합계의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
예산 10만 원·사용액 0원의 같은 기준에서 각 작업의 6만 원 승인은 조건을 통과한다. 서로 다른 주문 행에 둘 다 확정되면 합계는 12만 원이다. 이는 쓰기 편향의 가정이며 안정된 스냅샷만으로 전체 합계 규칙이 보장되는 것은 아니다.
이 예의 조건
- 예산 10만 원·사용액 0원의 같은 기준에서 각 작업의 6만 원 승인은 조건을 통과한다. 서로 다른 주문 행에 둘 다 확정되면 합계는 12만 원이다. 이는 쓰기 편향의 가정이며 안정된 스냅샷만으로 전체 합계 규칙이 보장되는 것은 아니다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 대상 | 원 설명 |
|---|---|
| 같은 읽기 기준 | 사용액 0원 할인 예산 10만 원 |
| T1: 주문 A | 0 + 6만 ≤ 10만 할인 6만 추가 |
| T2: 주문 B | 0 + 6만 ≤ 10만 할인 6만 추가 |
| 둘 다 확정된다면 | 6만 + 6만 = 12만 원 |
| 예산 위반 | 12만 원은 한도 초과 어떤 직렬 순서도 불가 |
| 연결 | 원 라벨·방향 |
|---|---|
| 같은 읽기 기준 → T1: 주문 A | 라벨 없는 연결 / 정방향 / 실선 |
| 같은 읽기 기준 → T2: 주문 B | 라벨 없는 연결 / 정방향 / 실선 |
| T1: 주문 A → 둘 다 확정된다면 | 라벨 없는 연결 / 정방향 / 실선 |
| T2: 주문 B → 둘 다 확정된다면 | 라벨 없는 연결 / 정방향 / 실선 |
| 둘 다 확정된다면 → 예산 위반 | 라벨 없는 연결 / 정방향 / 실선 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 스냅샷 한도 100k·현재 합계 0을 두 거래가 읽는다. |
|
|
| 각 60k 허용 각 거래는 60k만 더하면 한도 안이라고 판단한다. |
|
|
| 두 커밋 서로 다른 두 행의 60k가 함께 확정될 수 있다. |
|
|
| 불변식 위반 최종 120k는 100k 한도를 넘는다. |
|
|
같은 기준을 읽고 다른 행을 쓰면 합계가 넘을 수 있다 · 상태 따라가기
한도 100k·현재 합계 0을 두 거래가 읽는다. 모든 단계에서 T1·T2 읽기·행·합계의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
예산 10만 원·사용액 0원의 같은 기준에서 각 작업의 6만 원 승인은 조건을 통과한다. 서로 다른 주문 행에 둘 다 확정되면 합계는 12만 원이다. 이는 쓰기 편향의 가정이며 안정된 스냅샷만으로 전체 합계 규칙이 보장되는 것은 아니다.
이 예의 조건
- 예산 10만 원·사용액 0원의 같은 기준에서 각 작업의 6만 원 승인은 조건을 통과한다. 서로 다른 주문 행에 둘 다 확정되면 합계는 12만 원이다. 이는 쓰기 편향의 가정이며 안정된 스냅샷만으로 전체 합계 규칙이 보장되는 것은 아니다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 대상 | 원 설명 |
|---|---|
| 같은 읽기 기준 | 사용액 0원 할인 예산 10만 원 |
| T1: 주문 A | 0 + 6만 ≤ 10만 할인 6만 추가 |
| T2: 주문 B | 0 + 6만 ≤ 10만 할인 6만 추가 |
| 둘 다 확정된다면 | 6만 + 6만 = 12만 원 |
| 예산 위반 | 12만 원은 한도 초과 어떤 직렬 순서도 불가 |
| 연결 | 원 라벨·방향 |
|---|---|
| 같은 읽기 기준 → T1: 주문 A | 라벨 없는 연결 / 정방향 / 실선 |
| 같은 읽기 기준 → T2: 주문 B | 라벨 없는 연결 / 정방향 / 실선 |
| T1: 주문 A → 둘 다 확정된다면 | 라벨 없는 연결 / 정방향 / 실선 |
| T2: 주문 B → 둘 다 확정된다면 | 라벨 없는 연결 / 정방향 / 실선 |
| 둘 다 확정된다면 → 예산 위반 | 라벨 없는 연결 / 정방향 / 실선 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 스냅샷 한도 100k·현재 합계 0을 두 거래가 읽는다. |
|
|
| 각 60k 허용 각 거래는 60k만 더하면 한도 안이라고 판단한다. |
|
|
| 두 커밋 서로 다른 두 행의 60k가 함께 확정될 수 있다. |
|
|
| 불변식 위반 최종 120k는 100k 한도를 넘는다. |
|
|
각자는 동일한 과거 기준을 계속 읽을 수 있고 쓰는 행도 달라 단순 행 쓰기 충돌이 없지만, 둘 다 성공한 결과는 어느 직렬 순서로도 설명되지 않는다. 하나씩 실행했다면 뒤 작업은 이미 사용한 6만 원을 보고 12만 원을 거절해야 한다. 개별 행 잠금이나 안정된 스냅샷만으로 행 집합 전체의 합계 규칙을 자동 보장하지는 못한다.
개선 후보는 관련 작업을 모두 Serializable로 수행하고 충돌 시 다시 판단하거나, 모든 승인 작업이 같은 예산 관리 행을 보호하고 잔여 예산을 원자적으로 확인·차감하게 하는 것이다. 한 경로만 바꾸고 다른 경로가 보호 규칙을 건너뛰면 해결되지 않는다. 정식 데이터 모델을 바꾸는 처방을 이 예제만으로 확정하지 않고 업무 규칙과 모든 변경 경로를 먼저 확인한다.
Serializable은 잘못된 동시 완료를 거절할 수 있다
PostgreSQL의 직렬가능 스냅샷 격리(Serializable Snapshot Isolation, SSI)는 스냅샷 격리 실행에 읽기·쓰기 의존성 검사를 추가해 직렬가능성을 해칠 수 있는 트랜잭션을 중단시키는 방식이다. 뒤 작업이 쓴 내용을 앞 작업이 먼저 읽은 기준에서는 보지 못했다면, ‘앞 작업이 먼저 실행됐어야 한다’는 관계가 생길 수 있다. 이 관계가 서로 반대 방향으로 이어지는 위험을 살핀다.
할인 예산 예제에서는 둘 다 성공하도록 내버려 두지 않고 일부 작업이 중단될 수 있다. 새 기준으로 전체 작업을 재시도하면 다른 주문의 승인액을 보고 예산 초과를 판단한다. 재시도에는 SQL뿐 아니라 어떤 SQL과 값을 선택할지 정하는 애플리케이션의 앞선 판단도 포함된다. 이미 실패한 트랜잭션에서 읽은 결과를 최종 승인처럼 고객에게 확정 통지해서는 안 된다.
PostgreSQL의 SIReadLock은 SSI가 읽기 의존성을 추적하려고 남기는 표시다. 그 표시 자체가 다른 쓰기를 기다리게 하는 잠금은 아니다. 일반 표·행 잠금은 별도로 존재한다. SSI는 모든 트랜잭션을 한 줄로 세워 실행하거나 매번 완전한 그래프 순환만 찾는 구현으로 이해하지 않는다. 위험한 의존성 패턴을 보수적으로 검출하므로 실제로는 직렬화 가능한 실행도 중단될 수 있다.
중단은 곧 DBMS의 오작동이 아니다. 오류 종류를 구별하고 재시도 횟수·대기·실패 안내를 운영 계약으로 정한다. 충돌이 지속되면 반복만 늘리지 말고 트랜잭션 범위와 데이터 접근 구조를 조사한다. DB 밖의 결제·알림은 별도 식별자와 중복 방지·대사 없이는 같이 재시도하지 않는다.
읽은 숫자를 다시 쓰는 것과 조건부 변경은 다르다
트랜잭션의 네 성질(ACID)에서 두 주문이 같은 재고를 읽은 뒤 각각 계산한 오래된 값을 쓰면 갱신을 잃을 수 있다고 배웠다. 주문 O301·O302가 각각 한 개를 요청하는 별도 설명 상황에서 재고가 1이라면, 각자가 먼저 1을 읽고 나중에 계산한 0을 쓰는 것은 ‘한 주문만 성공’ 규칙을 표현하지 못한다.
다음은 실행 지시가 아니라 트랜잭션의 네 성질(ACID)에서 배운 조건부 변경의 의미를 읽는 예다.
UPDATE "재고"
SET "수량" = "수량" - 1
WHERE "상품번호" = 'P1' AND "수량" >= 1;이 문장은 상품 P1의 현재 수량이 1 이상이라는 조건을 만족할 때 수량을 하나 줄인다. 상품번호가 해당 재고 행 하나를 식별한다는 전제도 필요하다. PostgreSQL 18의 Read Committed에서 같은 행의 선행 변경이 커밋되어 수량이 0이 됐다면, 기다린 작업은 조건을 다시 검사하여 그 행을 변경하지 않을 수 있다. 애플리케이션은 변경 행 수가 1인지 확인하고, 주문 기록까지 같은 트랜잭션에서 성공시킨 경우에만 주문을 확정한다. 0행 변경인데도 주문을 성공 처리하면 조건부 SQL만으로는 막을 수 없다.
한 행 조건부 변경이 모든 다중 행 규칙을 해결한다는 뜻도 아니다. 앞의 할인 총액처럼 여러 행의 합계·부재를 근거로 판단하는 규칙은 그 집합 전체의 보호나 직렬성 검토가 필요하다. DB의 제약, 격리 수준, 애플리케이션의 결과 확인은 서로를 지워 버리는 대안이 아니라 각자 담당 경계를 지켜야 하는 수단이다.
완료 조건을 바꾸어 예측하기. 수량 1인 같은 재고 행에 T1이 위 변경문을 적용해 미확정 수량 0을 만들었고, T2가 같은 변경문을 보내 기다린다고 하자. PostgreSQL 18의 Read Committed라는 조건을 유지한 채, T1이 커밋할 때와 롤백할 때 T2의 변경 행 수를 각각 예상한다.
풀이 방향과 완성 답안 보기
해설과 교정. T1이 커밋하면 유효한 수량은 0이다. T2는 기다림 뒤 조건을 다시 검사해 0>=1이 거짓이므로 0행을 변경하고 주문을 확정하지 않는다. T1이 롤백하면 그 감소가 취소되어 수량은 다시 1이며, T2는 조건을 만족하는 한 행을 0으로 바꿀 수 있다. 이때도 T2의 주문 기록까지 같은 트랜잭션에서 성공해야 주문을 확정한다. 기다렸으니 무조건 실패와 SQL 실행이 끝났으니 무조건 주문 성공은 서로 다른 오답이다. 각각 Read Committed의 변경 조건 재검사와 이 블록의 변경 행 수·주문 확정 경계로 돌아가 고친다.
Read Committed의 조건 재검사와 조건부 변경의 확정 경계를 함께 읽고 두 결과를 설명한다.
미리 기다리게 할지, 뒤에서 충돌을 검사할지
낙관적 동시성 제어(Optimistic Concurrency Control, OCC)는 먼저 읽기와 계산을 진행하고, 완료 전에 필요한 충돌 검증을 통과한 작업만 확정하도록 하는 접근이다. 검증 대상에는 읽은 데이터와 변경할 데이터의 관계가 포함되며, 검증과 반영 사이에도 다른 작업이 끼어 보장을 깨지 못하도록 구현해야 한다. 읽은 행의 버전 번호가 그대로인지 확인하는 애플리케이션 기법은 그 한 형태의 재료이지, 행 집합 전체의 직렬가능성을 자동 인증하지는 않는다.
문구점에서 서로 다른 상품의 수정이 드물게 충돌한다면 먼저 진행하고 실패 때 다시 읽는 방식이 유리할 수 있다. 같은 인기 상품에 변경이 몰리면 버리는 계산과 재시도가 늘어날 수 있다. 반대로 미리 잠가 조정하면 대기를 감수하는 대신 이미 실패할 일을 길게 수행하는 낭비를 줄일 수 있다. 어느 방식도 모든 부하에서 우월하지 않다.
타임스탬프 순서 제어는 트랜잭션에 부여한 순서값에 맞추어 충돌 연산의 선후 관계를 통제하는 방법이다. 여기서 타임스탬프는 순서를 정하는 값이며 벽시계의 날짜·시각 자료형과 반드시 같지 않다. 기본 모형에서 순서 20의 작업이 항목을 이미 읽은 뒤 순서 10의 늦은 쓰기가 도착하면, 그 쓰기를 그대로 받아들이는 것은 정한 순서와 모순이므로 거절·재시도 대상이 된다. 실제 알고리즘은 읽기·쓰기 순서와 확정 전 노출도 별도로 관리한다.
잠금, 낙관적 검증, 여러 버전, 순서값은 모두 같은 차원의 제품 목록이 아니다. 동시성 제어를 비교할 때는 보장하는 격리, 충돌을 발견하는 시점, 대기와 중단 비용, 필요한 상태 정보와 회복 조건을 같은 축에 놓는다. PostgreSQL의 MVCC가 모든 의미에서 ‘아무 잠금 없는 OCC’라고 쓰지 않는다.
지난 버전은 영원히 남겨 둘 수 없다
PostgreSQL의 VACUUM은 더 이상 필요한 작업에 보이지 않는 오래된 행 버전의 공간 재사용과 가시성·트랜잭션 식별자 관리 등을 수행하는 유지보수다. 수정·삭제가 과거 버전을 즉시 없애지 않는 것은 그 버전을 읽는 작업이 있을 수 있기 때문이다. 반대로 아무도 필요로 하지 않는 버전을 계속 쌓으면 표·인덱스의 공간과 읽기 비용이 늘어난다.
오래 열린 트랜잭션이나 과거 상태를 필요로 하는 보존 조건은 버전 정리를 늦출 수 있다. PostgreSQL의 자동 유지보수가 켜져 있어도 완료 속도와 남은 오래된 상태를 관측해야 한다. ANALYZE의 분포 통계 수집과 VACUUM의 버전·공간 정리는 관련 있지만 같은 동작은 아니다.
일반 VACUUM으로 확보한 공간은 주로 DB 안에서 다시 쓴다. 언제나 파일이 줄어 운영체제에 용량이 반환되는 것은 아니다. 표를 다시 쓰는 VACUUM FULL은 더 강한 잠금과 추가 공간이 필요하므로 정기 유지보수와 같은 비용으로 생각하지 않는다. 또한 유한한 트랜잭션 번호가 순환할 때 아주 오래된 행이 미래의 행처럼 오인되지 않도록 과거 버전을 동결해 관리하는 책임도 있다. MVCC의 성능과 가시성 보장을 유지하려면 오래된 작업·정리 지연·버전 공간·식별자 수명까지 운영에서 연결해 보아야 한다.
로그와 저장 상태로 장애 복구를 판단한다
무엇이 고장 났는지에 따라 복구의 출발점이 달라진다
지금까지는 작업들이 동시에 살아 있을 때를 보았다. 이제 문구점 서버가 갑자기 꺼졌다고 하자. 메모리의 더티 페이지는 사라질 수 있고, 저장 파일에는 일부 변경만 반영돼 있을 수 있다. 커밋한 주문을 잃지 않으면서 미완료 주문을 확정된 주문처럼 보이지 않게 해야 한다.
트랜잭션 장애는 특정 트랜잭션이 오류·충돌 등으로 정상 확정되지 못하고 중단되는 경우다. 시스템 장애는 재시작 과정에서 휘발성 메모리의 상태를 신뢰할 수 없게 되는 경우다. 매체 장애는 데이터나 복구 기록을 둔 지속 저장장치의 내용 자체가 손상·유실되는 경우다. 각각 한 작업의 취소, 남은 파일·로그를 이용한 재시작, 살아 있는 별도 복사본에서의 복원이 주된 출발점이다.
여기에 잘못된 삭제를 정상 커밋한 업무 오류를 따로 생각한다. DBMS가 기록을 정확히 복구해도 그 삭제를 그대로 재현할 수 있다. 따라서 ‘DB가 다시 켜진다’, ‘물리적으로 일관된다’, ‘원하는 업무 상태로 돌아왔다’는 서로 다른 확인이다. 저장장치까지 잃었는데 같은 장치에 있던 로그만으로 살리겠다는 계획은 성립하지 않는다.
WAL은 데이터 파일보다 먼저 복구 근거를 남기는 약속이다
선행 기록 로그(Write-Ahead Logging, WAL)는 변경된 데이터 페이지를 지속 저장장치에 반영하기 전에 그 변경을 복구할 로그를 먼저 지속시키는 원칙이다. 여기서 로그는 누가 화면을 열었는지 적는 접속 기록과 다르다. DBMS가 데이터 변경과 완료 상태를 재구성할 수 있도록 정한 형식의 기록이다.
로그 강제 기록(flush)은 메모리에 준비한 로그가 필요한 지속성 보장을 갖춘 저장 계층까지 기록되도록 요구하는 동작이다. 메모리에 로그를 만들었다는 것과 전원이 꺼져도 살아남는다는 것은 다르다. 운영체제·저장 제어기·장치가 완료 응답의 지속성 약속을 실제로 지켜야 한다.
WAL의 핵심 순서는 ‘관련 로그의 지속 → 변경 데이터 페이지의 지속’이다. 모든 메모리 변경보다 로그를 먼저 파일에 써야 한다는 뜻은 아니다. 동기 커밋은 성공 응답 전에 정한 로그 지속성 조건의 완료를 기다리는 커밋 방식이다. 해당 트랜잭션의 커밋을 복원하는 데 필요한 로그가 지속돼야 한다. 로그의 선행 기록과 커밋 성공 응답의 내구성 조건을 함께 지켜야 한다.
커밋과 페이지 저장이 다른 시점이어도 주문을 지킬 수 있다
다음은 순서를 읽기 위한 독립 예제이며 실제 로그 출력이 아니다. 주문 A는 확정됐고 주문 B는 처리 중이었다. 저장장치는 정상이고 동기 커밋의 지속성 조건을 지켰다고 가정한다.
1. A의 변경 기록을 로그에 준비
2. A의 커밋을 포함한 필요한 로그가 지속 저장됨
3. A 성공 응답 ← A는 확정
4. B의 변경을 처리하지만 B는 미확정
5. 장애: 일부 데이터 페이지는 파일에 아직 미반영
6. 재시작: 남은 로그와 데이터 상태로 일관된 상태 복원
A의 확정 효과는 유지 / B를 확정된 주문처럼 노출하지 않음A의 모든 데이터 페이지를 3번 전에 저장하지 않아도, 지속된 로그로 빠진 변경을 재구성할 수 있다. 반대로 B의 페이지 조각이 파일에 있다고 B가 커밋했다고 판단하지 않는다. 페이지 반영 여부와 트랜잭션 완료 상태는 앞서 구별한 서로 다른 축이다.
3번 성공 응답이 서버에서 나갔지만 연결이 끊겨 고객이 받지 못할 수도 있다. 고객의 ‘시간 초과’만으로 DB의 취소를 단정할 수 없다. 같은 주문 식별자로 확정 여부를 확인하고 중복 확정을 막는 업무 처리가 필요하다. 복구를 위해 내부 변경을 다시 반영하는 일과 고객 주문·외부 결제를 새로 실행하는 일은 같지 않다.
동기 커밋을 빠르게 만드는 것과 내구성을 낮추는 것은 다르다
PostgreSQL 18에서 동기 커밋은 성공을 알리기 전에 요구된 WAL 기록 완료를 기다린다. 비동기 커밋은 커밋 로그의 지속 완료를 기다리지 않고 성공을 먼저 알릴 수 있게 하는 방식이다. 그 뒤 장애가 나면 최근에 성공으로 응답한 일부 트랜잭션이 사라질 수 있다. 정상적인 복구 절차가 DB를 일관되게 돌린다는 것과 최근 성공의 유실을 허용하지 않는다는 것은 다른 요구다.
그룹 커밋은 여러 트랜잭션의 완료에 필요한 로그를 한 번의 지속 기록 작업으로 함께 충족시키는 기법이다. 각 작업이 자기 기록의 완료를 확인한다면, 함께 기록한다는 이유만으로 동기 커밋의 내구성 약속을 버리는 것은 아니다. 모아서 쓰는 효율과 기다리는 지연 사이의 절충을 살핀다.
문구점의 주문·결제 내역에서 허용할 수 없는 유실을 설정 변경으로 몰래 받아들이지 않는다. PostgreSQL의 fsync는 운영체제에 파일 변경의 지속 기록을 보장하도록 요구할지 정하는 설정이다. 이를 끄는 것은 비동기 커밋과 동등한 선택이 아니다. 전자는 장애 시 일관된 복구에 필요한 쓰기 순서·지속성 보장까지 깨뜨려 손상을 일으킬 수 있다. 성능 개선은 먼저 느린 질의·불필요한 변경·자원 대기와 저장 지연을 관측한 뒤 업무의 내구성 조건 안에서 판단한다.
체크포인트는 재시작의 출발 범위를 관리한다
체크포인트(checkpoint)는 장애 후 복구의 출발점을 정할 수 있도록 데이터 반영 상태와 필요한 복구 정보를 기록하는 절차다. 구체적인 절차는 복구 알고리즘마다 다르다. PostgreSQL은 체크포인트 대상의 더티 페이지 반영과 WAL의 체크포인트 기록 등을 통해 재시작에 필요한 범위를 관리한다.
로그 순서 번호(Log Sequence Number, LSN)는 로그 안에서 기록의 위치·순서를 식별하는 값이다. PostgreSQL의 LSN은 WAL 스트림의 바이트 위치이며 시계 시간이 아니다. 재시작은 제어 정보에 있는 체크포인트를 찾아 필요한 변경 재반영의 시작 위치부터 기록을 읽는다.
체크포인트가 만들어졌다고 당시 진행 중인 모든 사용자 트랜잭션이 커밋된 것은 아니다. 모든 서비스를 매번 완전히 멈추는 절차라고 단정하지도 않는다. PostgreSQL은 관련 쓰기를 시간에 걸쳐 나누어 수행할 수 있다.
너무 잦은 체크포인트는 쓰기와 전체 페이지 이미지 기록 부담을 늘릴 수 있고, 너무 드물면 장애 후 처리할 로그량이 늘 수 있다. 재시작 시간, 정상 시 쓰기 부하, 로그 보존량을 함께 비교한다. 체크포인트 하나를 별도 백업본으로 간주하거나, 만들었으니 과거 WAL을 사람이 임의 삭제해도 된다고 해석하지 않는다.
UNDO와 REDO가 필요한 이유를 두 가지 저장 정책으로 읽는다
REDO는 로그에 남은 변경 중 복구 대상에 아직 반영되지 않은 효과를 다시 반영하는 처리다. UNDO는 취소해야 할 변경의 효과를 제거하는 처리다. 이 두 말의 필요성을 보기 위해, 여기서는 현재 값의 자리를 덮어쓰는 단순한 저장 모형과 시스템 장애를 전제한다.
STEAL은 미확정 변경이 든 페이지도 필요하면 메모리에서 내보내 데이터 파일에 기록할 수 있는 정책이다. NO-STEAL은 미확정 변경이 데이터 파일의 현재 값을 덮어쓰지 못하게 하는 정책이다. FORCE는 커밋할 때 해당 트랜잭션의 변경을 데이터 파일에 모두 지속시키도록 강제하는 정책이다. NO-FORCE는 커밋 때 해당 트랜잭션의 변경을 데이터 파일에 모두 지속시키도록 강제하지 않는 정책이다. 메모리를 언제 비우는가와 커밋 때 무엇을 강제하는가라는 서로 다른 축이다.
| 이 단순 저장 모형의 정책 | 시스템 장애 뒤 UNDO 대비 | 시스템 장애 뒤 REDO 대비 |
|---|---|---|
| NO-STEAL + FORCE | 미확정 덮어쓰기 없음 | 확정 변경의 데이터 파일 반영 완료 |
| NO-STEAL + NO-FORCE | 미확정 덮어쓰기 없음 | 미반영 확정 변경 복원 필요 |
| STEAL + FORCE | 미확정 덮어쓰기 취소 필요 | 확정 변경의 데이터 파일 반영 완료 |
| STEAL + NO-FORCE | 미확정 덮어쓰기 취소 필요 | 미반영 확정 변경 복원 필요 |
STEAL은 미확정 작업 때문에 메모리 교체가 막히는 제약을 줄이고, NO-FORCE는 커밋마다 많은 데이터 페이지를 강제로 쓰는 부담을 줄인다. 대신 복구 정보와 처리가 필요해진다. 표의 ‘필요 없음’은 이 모형의 시스템 장애 재시작에서 그 이유로 필요하지 않다는 뜻이다. 매체 유실·과거 백업 복원까지 로그가 필요 없다는 뜻은 아니다.
앞서 배운 여러 버전 구현은 미확정 버전의 가시성도 관리한다. 이 표를 보고 모든 제품이 같은 형태의 이전 값 덮어쓰기 UNDO 파일을 쓴다고 결론 내리지 않는다. 알고리즘과 저장 모형을 함께 명시해야 한다.
ARIES는 이력을 재현한 뒤 미완료 변경을 취소한다
ARIES(Algorithms for Recovery and Isolation Exploiting Semantics)는 WAL, 페이지별 로그 순서 정보, 취소 기록을 이용해 세밀한 동시 처리와 장애 복구를 함께 지원하는 복구 알고리즘 계열이다. 여기서는 원전이 설명한 기본 재시작을 읽는다. 최신 PostgreSQL의 구현 전체가 이 알고리즘과 같다고 선언하는 이름표가 아니다.
ARIES는 이력을 재현한 뒤 취소 대상을 제거한다 · 상태 따라가기
체크포인트 뒤 로그로 작업·dirty 페이지와 REDO 시작을 정한다. 모든 단계에서 거래·로그·페이지·복구의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
분석은 체크포인트 이후 로그로 작업·더티 페이지와 REDO 출발점을 정한다. REDO는 페이지 반영 상태를 확인해 필요한 미완료 변경도 포함한 이력을 재현한다. UNDO는 취소 대상의 효과를 제거하며 CLR로 진행을 기록한다. PostgreSQL 18의 전체 구현과 동일하다고 읽지 않는다.
이 예의 조건
- 분석은 체크포인트 이후 로그로 작업·더티 페이지와 REDO 출발점을 정한다. REDO는 페이지 반영 상태를 확인해 필요한 미완료 변경도 포함한 이력을 재현한다. UNDO는 취소 대상의 효과를 제거하며 CLR로 진행을 기록한다. PostgreSQL 18의 전체 구현과 동일하다고 읽지 않는다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 대상 | 원 설명 |
|---|---|
| 분석 | 체크포인트 이후 로그 작업 상태·더티 페이지 REDO 출발점 결정 |
| REDO | 페이지 반영 상태 대조 필요한 변경 이력 재현 미완료 변경도 포함 |
| UNDO | 취소 대상 변경 제거 CLR로 취소 진행 기록 |
| 연결 | 원 라벨·방향 |
|---|---|
| 분석 → REDO | 라벨 없는 연결 / 정방향 / 실선 |
| REDO → UNDO | 라벨 없는 연결 / 정방향 / 실선 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 분석 체크포인트 뒤 로그로 작업·dirty 페이지와 REDO 시작을 정한다. |
|
|
| REDO 이력 재현 필요한 미완료 변경도 포함하여 이력을 재현한다. |
|
|
| UNDO 취소 취소 대상의 변경을 제거하고 CLR로 취소 진행을 기록한다. |
|
|
| 확인 범위 정한 재시작 조건에서 완료 거래 효과와 취소 대상 제거를 대조한다. |
|
|
ARIES는 이력을 재현한 뒤 취소 대상을 제거한다 · 상태 따라가기
체크포인트 뒤 로그로 작업·dirty 페이지와 REDO 시작을 정한다. 모든 단계에서 거래·로그·페이지·복구의 위치를 유지하며 값과 허용 조건을 함께 확인합니다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
분석은 체크포인트 이후 로그로 작업·더티 페이지와 REDO 출발점을 정한다. REDO는 페이지 반영 상태를 확인해 필요한 미완료 변경도 포함한 이력을 재현한다. UNDO는 취소 대상의 효과를 제거하며 CLR로 진행을 기록한다. PostgreSQL 18의 전체 구현과 동일하다고 읽지 않는다.
이 예의 조건
- 분석은 체크포인트 이후 로그로 작업·더티 페이지와 REDO 출발점을 정한다. REDO는 페이지 반영 상태를 확인해 필요한 미완료 변경도 포함한 이력을 재현한다. UNDO는 취소 대상의 효과를 제거하며 CLR로 진행을 기록한다. PostgreSQL 18의 전체 구현과 동일하다고 읽지 않는다.
- 이 원문의 교육용 사례·조건 안에서 상태를 대조합니다. 실제 구축·실험 결과나 원문에 없는 성능 보장을 추가하지 않습니다.
관계도의 보충 설명
| 대상 | 원 설명 |
|---|---|
| 분석 | 체크포인트 이후 로그 작업 상태·더티 페이지 REDO 출발점 결정 |
| REDO | 페이지 반영 상태 대조 필요한 변경 이력 재현 미완료 변경도 포함 |
| UNDO | 취소 대상 변경 제거 CLR로 취소 진행 기록 |
| 연결 | 원 라벨·방향 |
|---|---|
| 분석 → REDO | 라벨 없는 연결 / 정방향 / 실선 |
| REDO → UNDO | 라벨 없는 연결 / 정방향 / 실선 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 분석 체크포인트 뒤 로그로 작업·dirty 페이지와 REDO 시작을 정한다. |
|
|
| REDO 이력 재현 필요한 미완료 변경도 포함하여 이력을 재현한다. |
|
|
| UNDO 취소 취소 대상의 변경을 제거하고 CLR로 취소 진행을 기록한다. |
|
|
| 확인 범위 정한 재시작 조건에서 완료 거래 효과와 취소 대상 제거를 대조한다. |
|
|
분석 단계는 어떤 작업을 취소해야 하고 어느 로그부터 다시 반영할 필요가 있는지 정한다. REDO는 커밋한 작업만 골라 새로 실행하는 절차가 아니다. ARIES의 REDO는 필요한 미완료 변경도 재현하고, 뒤의 UNDO가 취소 대상의 효과를 제거한다. 이미 페이지에 반영된 변경을 또 적용하지 않도록 페이지의 순서 정보와 로그를 대조한다.
보상 로그 기록(Compensation Log Record, CLR)은 ARIES에서 취소로 수행한 변경과 취소의 진행 정보를 남기는 로그 기록이다. 취소 도중 다시 장애가 나도 이미 진행한 취소를 재현하고 다음 지점부터 이어갈 수 있게 한다. CLR은 REDO 대상이지만 그 취소 자체를 다시 UNDO하는 기록은 아니다. 복구 중 재장애까지 생각해야 ‘한 번 재실행하면 되겠지’라는 설명을 넘어설 수 있다.
복구하다 다시 꺼져도 100을 90으로 되돌리지 않는다
ARIES의 기본 모형을 한 항목으로 줄여 읽어 보자. 실제 로그 출력이나 PostgreSQL 내부 실측이 아니다. 여기의 LSN 10·20·30은 선후 관계를 나타내는 가상 번호이며 PostgreSQL의 실제 바이트 위치 표기가 아니다. 페이지에 기록된 LSN은 그 페이지에 마지막으로 반영한 로그 위치를 뜻한다.
| 단계 | 지속된 로그 | 데이터 파일의 값·페이지 LSN | 메모리에서 수행할 일 |
|---|---|---|---|
| 출발 | 이전 기록 10까지 | 100, LSN 10 | T1이 100을 90으로 바꾸려 함 |
| 첫 장애 | 변경 기록 20까지, T1 커밋 없음 | 100, LSN 10 | 메모리 소실 |
| 첫 REDO 뒤 페이지 저장 | 변경 기록 20까지 | 90, LSN 20 | 필요한 변경을 다시 반영 |
| UNDO 후 두 번째 장애 직전 | 취소를 기록한 CLR 30까지 | 여전히 90, LSN 20 | 100, LSN 30인 더티 페이지는 아직 미저장 |
| 두 번째 REDO | CLR 30이 지속돼 있음 | 필요한 재반영 후 100, LSN 30으로 저장 가능 | 이미 한 취소를 재현 |
첫 재시작에서는 페이지 LSN 10보다 변경 로그 20이 뒤이므로 필요한 변경을 반영한다. 미완료 T1의 변경이라도 ARIES의 REDO에서는 90을 재현할 수 있다. 이 페이지를 파일에 저장하기 전에 로그 20이 지속돼 있다는 WAL 조건을 지킨다.
이어서 T1을 UNDO해 100으로 돌리고, 그 취소를 CLR 30에 기록한다. CLR 30이 지속됐지만 변경한 페이지가 저장되기 전에 다시 꺼졌다고 하자. 파일에는 여전히 90과 LSN 20이 남는다. 두 번째 재시작은 필요한 CLR 30의 효과를 다시 반영해 100을 복원할 수 있고, 페이지를 저장하기 전에 CLR 30의 지속도 확인한다.
CLR 자체를 다시 UNDO해 90으로 바꾸지 않는다. CLR의 다음 취소 위치는 이미 취소한 변경 20을 건너뛰도록 연결되므로 남은 취소와 작업 종료 처리를 이어간다. 이 예제에서는 그 항목에 더 취소할 앞선 변경이 없다. 이렇게 페이지의 반영 상태와 취소 진행을 구분하여 복구 중 재장애도 다룬다.
PostgreSQL 재시작에서는 로그와 행의 가시성을 함께 생각한다
PostgreSQL 18의 공식 문서는 WAL 재시작을 체크포인트에 연결된 REDO 위치에서 기록을 다시 처리하는 과정으로 설명한다. 앞에서 읽은 PostgreSQL의 MVCC는 행 버전과 트랜잭션 상태에 따라 읽기에 보일 데이터를 정한다. 따라서 ‘저장된 변경 기록이 있다’와 ‘사용자에게 확정된 행으로 보인다’를 구별해야 한다.
이 절의 ARIES 도식은 복구 이론을 이해하기 위한 별도 알고리즘이다. PostgreSQL의 재시작도 반드시 같은 세 단계와 같은 CLR 구조로 동작한다고 옮겨 적지 않는다. 반대로 PostgreSQL이 여러 버전을 사용한다는 이유로 WAL이나 원자성 보장이 불필요하다고 하지도 않는다.
또한 페이지를 쓰다가 전원이 끊기면 한 페이지의 일부만 기록될 수 있다. WAL의 전체 페이지 이미지는 복구를 위해 페이지 전체의 내용을 로그에 남긴 기록이다. PostgreSQL은 체크포인트 뒤 최초 변경 시의 이런 기록 등으로 부분 쓰기 상황의 복구를 돕는다. 그러나 저장 계층이 기록 완료를 거짓으로 보고하거나 필요한 WAL과 데이터가 함께 유실되는 일까지 무한히 보장하지 않는다. 알고리즘, 제품 버전, 설정, 저장장치의 지속성 가정이 복구 보장의 경계다.
재시작 복구와 백업 복원은 같은 작업이 아니다
물리 기본 백업(base backup)은 DB의 물리 파일 상태를 복원 출발점으로 사용할 수 있도록 정해진 절차로 확보한 백업이다. 실행 중인 DB 폴더를 아무 순서로 복사한 것과 같지 않다. 시점 복구(Point-in-Time Recovery, PITR)는 기본 백업과 이어지는 변경 기록을 사용해 선택한 과거의 일관된 시점까지 복원하는 방식이다.
PostgreSQL 18의 연속 보존 방식에서는 사용 가능한 물리 기본 백업과, 백업에 필요한 시작 지점부터 목표까지 이어진 WAL이 필요하다. SQL 문장 형태로 논리 내용을 내보내는 pg_dump 결과를 이 WAL 재생의 물리 기본 백업으로 바꾸어 부를 수는 없다. 증분 물리 백업을 쓰는 경우에도 필요한 이전 백업·결합 절차와 WAL 조건을 따로 충족해야 한다.
로그를 보존해도 백업이 없거나 필요한 구간이 빠졌으면 원하는 복구가 되지 않는다. 반대로 백업 파일만 있고 그 뒤 로그가 없으면 백업 이후의 주문을 그 방식으로 되살릴 수 없다. 체크포인트는 실행 중인 DB의 복구 관리 정보이며, 장애 영역 밖의 별도 백업을 대신하지 않는다.
잘못된 삭제를 정상 커밋했다면 최신 상태까지 모든 로그를 재생할 때 그 삭제도 다시 나타난다. 삭제 직전의 목표를 선택하고 주문·재고·금액의 업무 규칙과 외부 결제의 상태까지 대조해야 한다. 물리적 복구 성공을 곧바로 업무 서비스 재개 승인으로 바꾸지 않는다.
백업 종료 전으로 돌아갈 수 있는지 시간을 읽어 본다
문구점의 독립 복구 계획 예제를 보자. 복원 자료를 만드는 백업은 09:00에 시작해 09:10에 끝났고, 필요한 WAL을 빠짐없이 확보했다고 가정한다. 09:25에 잘못된 삭제를 커밋했다. 여기의 시간은 개념을 읽기 위한 가상 값이며 실제 복구 훈련 결과가 아니다.
09:08은 백업 종료 전이라 불가하며 09:20은 검토 가능하다. 필요한 WAL의 연속성과 백업·복원 결과도 확인한다.
PostgreSQL의 이 백업에서 출발하는 복구는 백업이 끝난 뒤의 시점이 목표여야 한다. ‘시작이 09:00이니 09:08도 가능하다’는 추론은 틀리다. 09:20은 시간 조건을 만족하지만, 필요한 WAL의 연속성·백업의 유효성·목표 설정과 복원 결과 검증까지 통과해야 한다. 시각만 맞는다고 성공이 보장되지는 않는다.
복원한 DB를 새로 변경하기 시작하면 이전 이력과 갈라지는 경로가 생긴다. PostgreSQL의 타임라인은 복구 후 새 변경 경로를 구별하는 WAL 이력의 분기다. 같은 시각이나 같은 파일 이름 일부만 보고 다른 경로의 기록을 섞지 않는다.
실제 계획에서는 어느 시점까지 주문 손실을 허용하는지, 얼마 안에 조회·주문 처리를 다시 열어야 하는지를 먼저 합의한다. 백업 생성 성공률뿐 아니라 격리된 환경의 복원 가능성, 주문 수·재고·금액 대조, 걸린 시간과 빠진 주문 범위를 확인해야 한다. 여기서는 독자가 복구 명령을 실행하지 않고도 필요한 자료와 판단 순서를 읽을 수 있도록 구성했다.
원인별 진단을 시험답안의 근거로 바꾼다
한 재고 예약을 메모리에서 복구 결과까지 잇는다
단계별 풀이. 이미 확정한 O101·O102는 그대로 두고, P1 재고 1개를 요청하는 새 예약 O301과 O302만 본다. 상품번호는 재고 한 행을 식별하며 PostgreSQL 18의 Read Committed, 주문기록과 재고 차감의 같은 트랜잭션, 필요한 로그의 동기 지속, 정상 저장장치를 가정한다. DB 밖의 결제·알림은 이 트랜잭션에 포함하지 않는다.
- 목록 조회의 SQL은 조건·정렬·개수를 말한다. DBMS는 이름과 자료형을 확인하고 계획을 고른다. 결과가 20행이어도 조사한 행·버퍼 페이지·장치 읽기는 각각 따로 센다.
- O301의 조건부 변경이 1행을 바꾸면 버퍼의 재고는 미확정 0이다. O302는 같은 변경을 기다린다. 이때 메모리 값 0이나 데이터 파일의 반영 여부만 보고 예약 완료라고 판단하지 않는다.
- O301의 주문기록까지 성공하고 커밋에 필요한 WAL이 지속된 뒤 성공을 알린다. O301의 모든 데이터 페이지가 파일에 반영되어야만 이 성공이 가능한 것은 아니다.
- O301이 커밋했다면 기다린 O302는 조건을 재검사해 0행을 바꾼다. O302를 예약 성공으로 처리하지 않는다. O301이 롤백했다면 재고 1을 기준으로 O302의 전체 판단을 이어갈 수 있다.
- O301 커밋 뒤 서버가 꺼져 일부 페이지 반영이 빠졌다면 살아 있는 로그·데이터와 트랜잭션 상태로 재시작한다. 확정 O301은 유지하고 미확정 예약은 확정처럼 노출하지 않는다. 성공 응답이 고객에게 도착하지 않았다는 별도 문제는 같은 예약 식별자로 확인한다.
독립 연습. 3단계의 동기 지속까지 끝났지만 O301 성공 응답만 유실된 직후 서버가 꺼졌다고 하자. 고객의 시간초과가 뜻하는 것, 정상 재시작 뒤 재고·예약을 대조할 근거, 새 예약을 다시 만들기 전에 할 확인을 세 문장으로 적는다.
전 경로의 해설과 잘못된 추론 교정 보기
시간초과는 고객이 성공을 받지 못했다는 관측이며 O301 롤백의 증거가 아니다. 명시한 동기 지속·정상 저장 조건에서 재시작은 O301의 확정 효과를 지켜야 하므로 커밋 상태와 같은 예약 식별자를 대조한다. 재고 0과 O301 기록의 일치를 확인한 뒤 결과를 안내하며, 확인되지 않은 채 새 식별자로 동일 주문을 추가하지 않는다.
파일에 재고 0이 없으니 예약 취소는 페이지 반영과 커밋의 두 축을 혼동했다. 변경문이 끝났으니 O302도 성공은 조건 재검사와 변경 행 수를 놓쳤다. 성공 응답이 없으니 결제까지 다시 실행은 DB의 확정·응답 수신·외부 효과를 같은 사건으로 섞었다. 각 문장에서 어떤 증거까지 확인했는지 먼저 표시하여 고친다. 이 풀이도 설명용이며 실 DB 실험이나 공식 채점표가 아니다.
느림·정확성·복구를 한 원인으로 뭉개지 않는다
처음 문구점의 세 문제로 돌아가자. 조회가 느리다는 사실만으로 인덱스를 추가하지 않고, 동시에 주문했다고 모든 처리를 표 전체 잠금으로 바꾸지 않으며, 서버가 꺼졌다고 무조건 과거 백업부터 덮어쓰지 않는다. 먼저 증거를 분리한다.
| 관찰된 문제 | 먼저 대조할 증거 | 원인에 맞는 검토 | 잘못된 지름길 |
|---|---|---|---|
| 고객 주문 조회 지연 | 추정·실제 행 수, 반복 횟수, 읽은 페이지, 정렬 임시 파일 | 통계·조건 의미·접근 경로·조인 순서와 메모리 | 반환 20행이니 20행만 읽었다고 간주 |
| 같은 재고를 중복 확정 | 읽기–판단–변경 경계, 변경 행 수, 격리·잠금 | 조건부 변경과 같은 트랜잭션의 주문 확정 | 마지막 SQL만 재시도 |
| 여러 주문의 할인 한도 초과 | 여러 행을 읽은 범위와 서로 다른 쓰기 | 직렬화 충돌 처리 또는 전체 규칙의 보호 | RR이므로 모든 업무 규칙이 안전하다고 단정 |
| 변경·대기가 계속 늘어남 | 잠금 대기, 오래 열린 작업, 과거 버전, 임시 파일·쓰기 | 짧은 트랜잭션, 정리 지연, 부하와 메모리의 동시 사용 | 메모리 한 설정을 모든 세션에 무제한 확대 |
| 장애 후 주문 상태 불명 | 커밋 기록·성공 응답 경계, 데이터·WAL의 생존 | 정상 재시작과 동일 주문 상태 확인 | 시간 초과를 DB 취소로 단정 |
| 삭제 이전으로 복원 필요 | 백업 유효성·종료, WAL 연속성, 타임라인·목표 | 격리 복원과 업무·외부 처리 대조 | 체크포인트를 별도 백업처럼 사용 |
변경안은 같은 업무량·데이터 범위에서 결과의 정확성과 비용을 함께 비교한다. 조회시간을 줄이면서 재고 불변식이나 내구성을 낮췄다면 같은 품질의 개선이 아니다. 계획·통계·대기·복구 근거를 남겨야 다른 사람이 판단을 재현할 수 있다.
DBMS 활용의 답안은 구성요소에서 판단 근거까지 이어진다
DBMS 내부구조를 활용하는 답안은 구성요소 목록만으로 끝나지 않는다. 다음 순서로 문제의 요구를 실제 통제 지점까지 연결한다.
- 요구와 경계를 정한다. 조회의 조건·정렬·결과 범위, 한 트랜잭션에서 지킬 업무 규칙, 허용 가능한 데이터 유실·중단 시간을 구분한다.
- 질의 경로를 설명한다. SQL의 구문·의미 확인과 카탈로그, 재작성, 논리 연산과 물리 계획, 실행기·버퍼의 역할을 연결한다.
- 비용을 증거로 검토한다. 카디널리티·분포·상관을 반영한 추정과 실제 행 수·반복·페이지·임시 파일을 대조하고 접근 경로·조인·메모리의 조건을 판단한다.
- 동시 실행의 정확성을 검토한다. 잠금 범위·보유 기간·교착, 버전 가시성·격리 이상·쓰기 편향을 업무 규칙에 대입하고 오류 시 전체 트랜잭션 재시도를 설계한다.
- 완료와 복구를 검토한다. 커밋 로그의 지속과 응답 경계, 체크포인트·재시작 알고리즘, 백업·WAL 연속성·시점 복구의 조건을 구분한다.
- 변경의 효과와 한계를 남긴다. 제품 버전과 설정·저장 가정을 명시하고 같은 조건에서 성능과 업무 정확성, 복원 가능성을 확인한다.
이 절에서는 DBMS의 내부구조를 구체 이론과 자체 답안으로 연결한다. 문항과 계산은 저자가 만든 설명용 사례이며 실제 시험 출제·실 DB 실험·사람 검수를 주장하지 않는다.
10점 문제: SQL이 결과가 되기까지
DBMS가 SQL 질의를 처리하는 과정을 설명하고, 비용 기반 실행계획의 선택과 검증 시 유의사항을 설명하시오.
자체 작성한 10점 연습문제다. ‘처리 과정’에는 SQL의 의미를 확인하는 단계부터 실행기가 결과 행을 만드는 단계까지 포함한다. ‘선택과 검증’은 최적화기가 항상 최선이라는 선언이 아니라 추정에 따른 선택과 실제 관찰의 차이를 설명하라는 요구다.
답안에서는 해석·재작성, 논리·물리 계획, 통계와 비용, 실행·검증의 네 논점을 짧게 잇는다. 동시성·복구 전체를 다시 쓰느라 문제에서 묻는 질의 경로를 빠뜨리지 않는다.
풀이를 열기 전에 해석·계획·추정·실행의 역할을 연결해 짧은 답안을 써 본다. 선택한 계획을 믿기 전에 어떤 실제 증거를 대조해야 하는지도 덧붙인다.
풀이 방향과 완성 답안 보기
10점 목차: 네 논점의 연결
- 구문·의미 확인, 카탈로그와 질의 재작성
- 논리 연산과 물리 구현 후보의 구별
- 카디널리티·통계·상관에 따른 비용 추정과 선택의 한계
- 실행기·버퍼의 결과 생성과 실제 계획 검증
10점 도식: 의미를 지키며 비용을 줄인다
앞의 SQL을 결과로 만드는 경로와 실행을 지키는 페이지·동시성·복구 책임 그림을 함께 읽는다. SQL의 구문·이름·자료형은 카탈로그와 대조하고, 재작성한 질의의 같은 의미를 지키며 행 수·분포·상관 추정과 비용으로 물리 후보를 비교한다. 접근 경로·조인·정렬을 선택한 실행기는 버퍼·페이지와 협력해 실제 결과를 만든다.
그 뒤 추정 대 실제 행 수, loops, 읽기와 임시 파일을 대조한다. 검증으로 통계·후보를 재검토하더라도 중복·NULL·정렬 의미는 유지한다.
직선은 설명 순서이며 모든 DBMS가 동일한 내부 파일이나 모듈을 강제한다는 뜻은 아니다. 피드백으로 통계를 정비하거나 후보를 다시 검토해도 SQL의 중복·NULL·정렬 의미는 유지해야 한다.
10점 답안: 같은 결과를 만드는 실행 방법을 선택한다
DBMS는 SQL의 문법을 구문 분석하고 카탈로그를 참조해 표·열·자료형 등의 의미를 확인한다. PostgreSQL의 일반 뷰는 저장된 결과 집합으로 간주하지 않고 정의된 질의의 재작성을 거쳐 처리한다. 의미 확인과 재작성은 결과 의미를 지키는 실행계획 선택의 출발점이다.
논리 연산은 조건으로 행을 남기기, 출력 열 구성, 입력의 결합 등 무엇을 계산하는지를 표현한다. 물리 계획은 순차·인덱스 접근, 중첩 반복·해시·병합 조인과 정렬 등 실제 수행 방법을 정한다. 후보를 바꾸더라도 중복·NULL·외부 조인의 행 보존과 요구한 정렬을 깨뜨려서는 안 된다.
비용 기반 최적화는 통계로 추정한 행 수·분포와 연산 비용을 이용한다. 열의 상관을 독립이라고 가정하면 여러 조건의 결합 행 수를 과소 추정할 수 있고, 오래되거나 부족한 통계는 조인 순서·메모리 선택에도 영향을 준다. 비용은 실제 밀리초가 아니며 후보 탐색도 제한될 수 있으므로 선택된 계획이 언제나 실제 최선이라는 보장은 없다.
실행기는 선택된 계획의 연산으로 행을 만들고 버퍼의 페이지를 이용한다. LIMIT 20이어도 앞 정렬이 많은 행을 읽을 수 있다. EXPLAIN의 추정과 실제 행 수를 대조하고, 반복 노드의 실제 평균 행 수에는 loops를 곱해 총 처리량을 읽으며 페이지 접근·정렬 임시 파일도 확인한다. EXPLAIN ANALYZE는 실제 실행하므로 변경 SQL의 부작용까지 통제해야 한다.
내 답안과 대조하기
“최적화기가 가장 빠른 계획을 고른다”만 적으면 무엇을 추정하고 어디서 빗나갈 수 있는지 드러나지 않는다. “통계로 행 수·분포·상관을 추정해 후보 비용을 비교하고, 같은 조건의 실제 행 수·반복·페이지·임시 파일과 대조한다”처럼 판단 근거를 잇는다. 더 빠른 방법을 고르더라도 SQL의 중복·NULL·정렬 의미를 지키는 조건은 남긴다. 이는 자체 연습을 위한 교정 예이며 공식 채점 기준이 아니다.
최적화기의 추정 한계와 실행계획의 단위로 돌아가 부족한 근거를 보완한다.
25점 문제: 조회·재고·장애를 함께 진단한다
문구점 주문 시스템에서 고객별 최근 주문 조회가 느리고, 여러 주문의 동시 처리에서 재고 및 할인 한도 규칙 위반이 우려된다. 서버 장애 후에는 확정 주문의 보존과 잘못된 삭제 이전의 복구가 필요하다. DBMS의 질의 처리·동시성 제어·복구 구조를 활용한 진단 및 개선 방안을 설명하시오. PostgreSQL 18의 구현 특성과 일반 이론의 경계를 구분하시오.
자체 작성한 25점 연습문제다. 읽기가 느린 원인, 동시 변경의 잘못, 시스템 장애, 정상 커밋된 잘못된 삭제는 서로 다른 문제다. 원인마다 관찰할 증거와 개입 지점을 나누어야 한다.
조회에서는 행 수 추정·실제 처리와 접근 경로를, 동시성에서는 한 행 조건과 여러 행 불변식의 차이를, 복구에서는 커밋 내구성과 과거 시점 복원의 차이를 답한다. PostgreSQL의 RR·SSI와 WAL 설명에 일반 2PL·ARIES를 섞어 동일한 제품 구현처럼 표시하지 않는다.
풀이를 열기 전에 조회 지연, 한 행 재고, 여러 행 할인 한도, 갑작스러운 장애, 잘못된 삭제를 나누어 본다. 각 문제에 확인할 증거와 보장의 경계를 하나씩 연결한 뒤 답안과 대조한다.
풀이 방향과 완성 답안 보기
25점 목차: 여덟 판단을 끝까지 연결
- 업무 요구·장애 유형·보장 범위의 분리
- 질의 처리 경로와 통계·상관·실제 계획 기반 진단
- 접근·조인·메모리·버퍼의 조건별 개선
- 재고의 원자적 조건부 변경과 잠금·교착·재시도
- MVCC·격리 수준·쓰기 편향과 다중 행 규칙 보호
- WAL·커밋 지속·응답 유실과 내구성 조건
- 체크포인트·재시작과 ARIES/제품 구현의 구별
- 백업·WAL 연속성·PITR의 실행 가능 조건과 업무 검증
25점 도식: 다른 문제에 다른 근거를 연결한다
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
같은 문구점의 요구에서 두 갈래를 읽는다. 조회는 SQL 의미→통계·계획→실제 실행을, 재고·한도는 트랜잭션 경계→통제→결과 확인을 따른다. 이는 자체 문제의 답안 관계이며 공식 채점 기준이 아니다.
상자 안의 대상과 화살표의 조건을 함께 읽으세요. 좁은 화면에서는 같은 관계를 세로로 재배치한 도식을 보여 줍니다.
갑작스러운 장애에서는 살아 있는 WAL·데이터로 재시작한다. 잘못된 삭제 이전으로 돌아가려면 기본 백업·연속 WAL·목표·타임라인을 대조하고 업무 결과를 검증한다. 두 갈래를 같은 복구 버튼으로 보지 않는다.
세 갈래는 역할의 구분이지 서로 독립된 튜닝 버튼이 아니다. 예를 들어 오래 열린 작업은 잠금·지난 버전의 정리를 방해하고, 메모리 확대는 여러 세션이 동시에 사용하는 전체량에 영향을 준다.
25점 답안: 빠른 조회, 올바른 확정, 복구 가능한 상태
1. 요구와 보장 범위를 분리한다. 최근 주문 조회의 조건·정렬·결과 범위를 확정하고, 재고 비음수와 할인 총액 한도처럼 지킬 규칙을 구분한다. 허용 유실·중단 시간도 정한다. 트랜잭션 중단, 메모리 상태를 잃는 시스템 장애, 데이터·로그 매체 유실과 잘못된 삭제의 정상 커밋을 같은 복구 문제로 취급하지 않는다.
2. 질의 처리와 추정의 근거를 진단한다. SQL의 구문·의미 확인 및 카탈로그 참조, 재작성, 논리 연산과 물리 후보 선택, 실행기의 결과 생성을 추적한다. 고객·조건별 행 수와 열 사이 상관이 통계에 반영되는지 보고 추정·실제 행 수를 비교한다. 실제 30행이 평균이고 loops가 10이면 약 300행 처리로 읽는다. 비용 수치를 밀리초로 해석하거나 부모·자식의 포함 비용을 단순 합산하지 않는다.
3. 조건에 맞는 수행 방법을 검토한다. 적은 주문을 찾을 때의 인덱스 경로와 많은 행을 읽을 때의 순차 경로, 작은 바깥 입력의 반복 탐색·동등 조건 해시·정렬된 입력 병합을 비교한다. 결과 의미를 지키며 불필요한 입력을 줄인다. 정렬·해시의 임시 파일과 버퍼 접근을 함께 살피며 work_mem을 서버 전체 한도로 오인하지 않는다. 여러 연산·세션의 메모리 합계를 고려하고 추정 개선과 실제 성능을 같은 조건에서 확인한다.
4. 재고 확정과 대기를 통제한다. 상품별 단일 재고 행에서는 수량이 1 이상일 때만 1을 줄이는 조건부 변경을 사용하고, 변경 행 수가 1인지 확인해 주문 기록까지 같은 트랜잭션으로 확정한다. PostgreSQL Read Committed의 경쟁 변경 후 조건 재검사 경계를 적용한다. 여러 행의 잠금 순서를 일관되게 하고 사용자 대기를 트랜잭션 밖으로 분리한다. 교착·직렬화 실패 때는 새로운 트랜잭션에서 읽기·판단부터 재시도하되 외부 결제의 중복 실행을 막는다.
5. 다중 행 규칙에는 격리의 한계를 적용한다. MVCC는 버전 가시성을 제공하지만 모든 이상이나 잠금을 없애지는 않는다. PostgreSQL의 Read Committed는 문장별, Repeatable Read는 트랜잭션 내 스냅샷을 기반으로 읽으며 후자도 쓰기 편향이 가능하다. 한도 10만 원을 함께 확인한 두 작업이 서로 다른 행에 6만 원씩 확정하면 총 12만 원이 될 수 있다. 관련 처리 경로 전체에서 Serializable의 충돌 중단·재시도를 적용하거나, 한도 집합의 판단을 보호하는 공통 상태와 잠금 등으로 규칙을 보장한다. SSI의 비차단 의존성 표시는 일반 쓰기 잠금과 구별한다.
6. 확정 주문의 내구성을 지킨다. WAL은 변경 데이터 페이지의 지속 반영에 앞서 필요한 로그를 지속시킨다. 동기 커밋은 성공 전 커밋 복원에 필요한 기록을 기다리므로 모든 데이터 페이지를 커밋마다 저장하지 않아도 된다. 비동기 커밋의 최근 성공 유실 허용과 fsync 해제의 손상 위험은 다르다. 응답 시간 초과는 취소 증거가 아니므로 같은 주문 식별자로 확정 상태를 확인한다.
7. 재시작의 범위와 알고리즘을 구별한다. 체크포인트는 복구 시작 범위를 관리하며 모든 사용자 작업의 커밋이나 별도 백업을 뜻하지 않는다. 빈도에 따른 정상 쓰기 부하와 재시작 처리량을 함께 평가한다. 일반 ARIES는 분석 후 필요한 미완료 변경까지 REDO하고 취소 대상은 UNDO하며 CLR로 취소 진행을 남긴다. PostgreSQL 18의 WAL 재시작·MVCC 설명을 이와 동일한 세 단계 구현이라고 복사하지 않고 제품·설정·저장 계층의 지속성 가정을 명시한다.
8. 과거 복원의 자료와 결과를 검증한다. 유효한 물리 기본 백업과 필요한 시작부터 목표까지 이어진 WAL, 올바른 타임라인을 확보한다. 09:10에 끝난 백업으로 09:08을 목표로 삼을 수 없으며, 09:25 삭제 이전인 09:20을 택해도 자료 연속성과 복원 결과를 확인해야 한다. SQL 논리 덤프나 체크포인트를 이 물리 백업의 대체물로 쓰지 않는다. 격리된 복원 환경에서 주문·재고·금액과 외부 결제를 대조하고 실제 유실 범위·복구 시간을 확인한 뒤 업무 재개를 판단한다.
내 답안과 대조하기
“인덱스·MVCC·WAL을 사용하면 빠르고 안전하다”는 답은 도구 이름만 있고 적용 조건이 빠져 있다. 조회에는 추정과 실제 처리량, 재고에는 조건 재검사·변경 행 수·같은 트랜잭션의 주문 확정, 할인 한도에는 여러 행 규칙의 보호를 연결한다. 장애 뒤에는 커밋 기록과 데이터의 생존을, 삭제 이전 복원에는 백업 종료·연속 WAL·목표·타임라인과 업무 결과를 확인한다. PostgreSQL 18의 설명을 일반 2PL·ARIES와 같은 구현이라고 옮기지 않는다. 이는 교재가 만든 답안 교정 예이며 공식 채점 기준이 아니다.
원인별 증거 표와 ARIES와 PostgreSQL 재시작의 구분으로 돌아가 내 답안에 빠진 경계를 확인한다.
근거의 판본과 복습 경로
근거의 판본과 이 절에서 사용한 범위
제품 설명의 기준일은 2026-09-05, 판본은 PostgreSQL 18이다. 웹사이트의 전체 배너 날짜를 각 문서의 최초 발행일이나 이 절의 기술 효력 시작일로 해석하지 않는다.
- 질의 경로: PostgreSQL 공식 질의의 경로, 계획기, 실행기, 표 표현식.
- 추정과 실행 관측: 공식 계획 통계, EXPLAIN 해설, 자원 관리.
- 제품의 동시성: 공식 격리 수준, 명시적 잠금, 직렬화 실패 처리, VACUUM.
- 일반 이론: Philip A. Bernstein·Vassos Hadzilacos·Nathan Goodman, Concurrency Control and Recovery in Database Systems (1987), 저자 공개본의 2·3·4·6장. 단일 버전 모형·복구 조건과 잠금 변형의 범위를 명시했으며, 당시의 제품 보급 현황을 최신 사실로 옮기지 않았다.
- SSI의 원리: Dan R. K. Ports·Kevin Grittner의 2012 PVLDB 원전 §§2–3·5.1. PostgreSQL 9.1을 설명한 역사 문헌이며 현재 18의 구현 주장은 공식 18 문서와 대조했다.
- 복구: 공식 WAL, 비동기 커밋, 체크포인트·WAL 구성, PITR; C. Mohan의 1999 VLDB 원전 §3.2의 ARIES와 CLR.
설명·문구점 데이터·문제·도식은 이 교재가 만든 것이다. 원전의 그림·문항·표를 복제하거나 다시 그리지 않았다. PostgreSQL의 라이선스는 문서 사용에도 고지 조건을 둔다. 저자 공개 책과 VLDB 논문에는 개인·교실 이용 및 재배포 제한이 있으므로 원문 PDF를 공개 교재의 부속 파일로 재배포하지 않는다. 출처 확인은 공개 교재 전체의 권리 승인과 다르다.
다시 읽을 때 붙잡을 하나의 흐름
SQL은 원하는 결과를 말하고, DBMS는 의미를 확인해 비용을 비교한 뒤 실제 행을 만든다. 추정이 빗나가는 이유는 값의 분포·상관과 반복·메모리 비용에서 찾고, 더 빠른 계획도 결과 의미를 지켜야 한다.
함께 변경할 때는 어느 범위의 값을 보고 어떤 규칙을 지키는지 살핀다. 한 행의 원자적 변경, 여러 행의 직렬성, 잠금 대기와 버전 가시성은 서로 다른 경계다. 실패한 판단을 안전하게 다시 시작하는 것까지 처리의 일부다.
장애 뒤에는 지속된 근거로 확정 효과를 지키고 미완료 효과를 분리한다. WAL·체크포인트·복구 알고리즘을 이해한 뒤, 매체 유실이나 잘못된 과거 상태에는 별도 백업과 이어지는 기록이 필요함을 구별한다. 빠른 조회·올바른 동시 확정·복구 가능한 저장은 같은 업무 규칙을 서로 다른 지점에서 지키는 책임이다.
적용 연습: 필터를 옮겼더니 고객이 사라진다
별도의 읽기 예제에서 고객 표에는 C1·C2·C3이 각 한 행씩 있다. 주문 표에는 (주문번호 O401, 고객번호 C1, 주문금액 1000)과 (O402, C2, 0) 두 행만 있고 C3의 주문은 없다. 주문번호는 NULL이 아닌 고유 식별자이며 저장된 주문금액에도 NULL은 없다. c와 o는 각각 고객·주문 표를 짧게 부르는 별칭이다. 아래 두 문장의 고객번호·주문번호·금액 결과를 적고, ‘모든 고객을 남기면서 양수 주문만 연결한다’는 요구에 맞는 문장을 고르라. 이어 A의 WHERE를 o."주문금액" > 0 OR o."주문번호" IS NULL로 바꾸면 B와 같은 결과가 되는지 C2와 C3을 따로 추적하라. 데이터 변경이나 동시 실행은 없는 조건이다.
-- A: 조인 결과에서 양수 금액만 남긴다.
SELECT c."고객번호", o."주문번호", o."주문금액"
FROM "고객" AS c
LEFT JOIN "주문" AS o ON c."고객번호" = o."고객번호"
WHERE o."주문금액" > 0
ORDER BY c."고객번호", o."주문번호";-- B: 양수 주문만 짝으로 인정하고 모든 고객을 보존한다.
SELECT c."고객번호", o."주문번호", o."주문금액"
FROM "고객" AS c
LEFT JOIN "주문" AS o
ON c."고객번호" = o."고객번호" AND o."주문금액" > 0
ORDER BY c."고객번호", o."주문번호";짝 찾기·NULL 보충·필터를 차례로 적용한 풀이 보기
A는 (C1, O401, 1000) 한 행만 남긴다. C2는 O402와 짝이 생겼지만 0 > 0이 거짓이어서 제거된다. C3은 주문 열이 NULL인 보충 행을 얻지만 NULL > 0도 참이 아니므로 WHERE를 통과하지 못한다.
B는 (C1, O401, 1000), (C2, NULL, NULL), (C3, NULL, NULL)의 세 행이다. C2의 0원 주문은 ON 조건에 맞는 짝이 아니므로 고객 C2를 오른쪽 NULL로 보충한다. C3도 짝이 없어 보충한다. 따라서 B가 요구를 만족하며, 출력 NULL을 C2의 실제 주문금액 0으로 바꾸어 읽으면 안 된다.
A에 OR o."주문번호" IS NULL을 더한 변형은 C1과 C3의 두 행만 남긴다. C2는 이미 O402와 짝지어진 결과이므로 주문번호가 NULL이 아니며, 금액 조건도 거짓이라 여전히 탈락한다. 조인 뒤 필터로 탈락한 행을 LEFT JOIN이 나중에 다시 NULL로 보충하지는 않는다. 그러므로 ‘NULL을 허용했으니 모든 고객이 복원된다’는 교정은 충분하지 않다. 의미를 지키는 질의 변환으로 돌아가 업무 요구와 조건의 위치를 먼저 고정한다.