데이터베이스 저장 구조: 페이지·익스텐트·세그먼트와 버퍼 캐시


1장. 한 행을 조회했는데 왜 여러 페이지를 읽을까#

다음 SQL을 실행했다고 하겠습니다.

SELECT customer_id, name, address
FROM customer
WHERE customer_id = 'M001';

결과는 단 한 행입니다.

M001 | 김하늘 | 서울

그렇다면 데이터베이스도 저장 장치에서 이 한 행에 해당하는 몇십 바이트만 읽었을까요?

실제로는 그렇지 않을 수 있습니다.

데이터베이스는 일반적으로 행 하나보다 더 큰 페이지 또는 블록 단위로 데이터를 읽고 관리합니다.

인덱스를 이용했다면 인덱스 페이지도 읽어야 합니다.

필요한 데이터 페이지가 메모리에 없다면 저장 장치에서 페이지를 가져와야 합니다.

큰 데이터가 별도 영역에 저장되어 있다면 추가 접근도 발생할 수 있습니다.

즉 사용자가 보는 것은 한 행이지만 내부에서는 다음과 같은 작업이 발생할 수 있습니다.

SQL 실행
↓
인덱스 페이지 탐색
↓
행 위치 확인
↓
데이터 페이지 접근
↓
필요한 열 읽기
↓
결과 한 행 반환

이 차이를 이해하면 다음과 같은 질문에 답하기 쉬워집니다.

결과는 한 건인데 왜 조회가 느린가?

인덱스를 만들었는데 왜 여전히 많은 읽기가 발생하는가?

같은 SQL을 두 번째 실행했더니 왜 갑자기 빨라졌는가?

답의 상당 부분은 페이지와 버퍼 캐시에 있습니다.


2장. 논리적인 테이블과 물리적인 저장 구조는 다르다#

개발자가 보는 테이블은 다음과 같습니다.

주문ID 회원ID 주문일 상태
O001 M001 2026-10-01 완료
O002 M002 2026-10-01 접수

하지만 저장 장치에는 이런 표 모양 그대로 기록되는 것이 아닙니다.

개념적으로 다음과 같은 계층을 생각할 수 있습니다.

테이블의 행
↓
페이지 또는 블록
↓
여러 페이지의 할당 단위
↓
테이블이나 인덱스의 저장 영역
↓
논리 저장 영역
↓
실제 파일

특히 Oracle 계열의 용어로 설명하면 다음과 같은 구조를 볼 수 있습니다.

Row
↓
Block
↓
Extent
↓
Segment
↓
Tablespace
↓
Datafile

다만 이 용어와 계층을 모든 DBMS에 그대로 적용해서는 안 됩니다.

PostgreSQL, SQL Server, MySQL, SQLite 등은 저장 구조와 용어가 서로 다릅니다.

핵심은 특정 제품 이름을 외우는 것이 아니라 행보다 큰 저장 단위로 데이터를 관리한다는 사실입니다.


3장. 페이지와 블록은 데이터베이스 저장과 I/O의 핵심 단위다#

여러 DBMS에서는 일정 크기의 페이지 또는 블록에 여러 행이나 인덱스 항목을 저장합니다.

개념적으로 다음과 같습니다.

데이터 페이지 A
├─ 주문 O001
├─ 주문 O002
├─ 주문 O003
└─ 남은 공간

다음 페이지에는 다른 행이 들어갑니다.

데이터 페이지 B
├─ 주문 O004
├─ 주문 O005
├─ 주문 O006
└─ 남은 공간

사용자가 O002 하나를 조회하더라도 DBMS는 보통 O002 몇 바이트만 따로 읽지 않습니다.

O002가 포함된 페이지가 필요합니다.

O002 조회
↓
O002가 포함된 페이지 A 필요
↓
페이지 A 접근

따라서 결과 행 수와 실제 내부 페이지 접근량은 다를 수 있습니다.


4장. 페이지 크기는 모든 DBMS가 같지 않다#

페이지 크기를 공부할 때 특정 숫자를 절대적인 규칙으로 외우면 안 됩니다.

제품과 설정에 따라 차이가 있기 때문입니다.

예를 들어 PostgreSQL의 일반적인 데이터 페이지는 8KB 구조를 중심으로 설명되지만, 다른 DBMS에서는 다른 크기와 구조를 사용할 수 있습니다.

Oracle 역시 데이터베이스 블록 크기를 구성에 따라 사용할 수 있습니다.

SQLite는 데이터베이스 파일의 페이지 크기를 설정하고 확인할 수 있습니다.

따라서 다음처럼 이해하는 것이 안전합니다.

페이지 크기
=
DBMS와 설정에 따라 달라지는 물리 관리 단위

중요한 것은 숫자 자체보다 페이지 크기가 다음과 같은 요소에 영향을 준다는 점입니다.

  • 한 페이지에 들어가는 행 수
  • 스캔 시 필요한 페이지 수
  • 버퍼 캐시 효율
  • 행 변경 시 공간 여유
  • 인덱스 구조
  • 저장 공간 사용량

5장. 페이지 안에는 사용자 데이터만 들어 있지 않다#

페이지 크기가 8KB라고 해서 정확히 8KB를 모두 업무 데이터에 사용할 수 있는 것은 아닙니다.

페이지 안에는 DBMS가 관리하기 위한 정보도 필요합니다.

개념적으로는 다음과 같은 구조를 생각할 수 있습니다.

flowchart LR
    A["페이지 헤더"] --> B["행 위치 정보"]
    B --> C["사용 가능한 공간"]
    C --> D["행 데이터"]

실제 내부 구조는 제품마다 다르지만 보통 다음 종류의 정보가 필요합니다.

영역 역할
페이지 헤더 페이지 상태와 관리 정보
슬롯·항목 정보 페이지 안에서 행 위치를 찾는 정보
빈 공간 새 행이나 행 증가에 사용할 공간
행 데이터 실제 컬럼 값과 내부 행 정보

따라서 다음 계산은 실제 저장 공간과 정확히 같지 않습니다.

행 크기 × 행 수

페이지 관리 정보와 여유 공간, 행 헤더, 인덱스, 버전 정보 등의 비용이 추가될 수 있습니다.


6장. 100바이트 행 80개가 꼭 8KB에 들어가는 것은 아니다#

단순 계산을 해보겠습니다.

한 행의 업무 데이터가 약 100바이트라고 가정합니다.

100바이트 × 80행
=
8,000바이트

8KB 페이지에 거의 들어갈 것처럼 보입니다.

하지만 실제로는 페이지 헤더와 행 관리 정보가 필요합니다.

행마다 내부 오버헤드가 있을 수도 있습니다.

공간을 일부 남겨 놓을 수도 있습니다.

따라서 실제 한 페이지에 들어가는 행 수는 단순 계산과 다를 수 있습니다.

이 차이가 누적되면 대규모 테이블에서는 상당한 저장 공간 차이가 발생할 수 있습니다.


7장. 페이지에 빈 공간을 일부 남겨 두는 이유#

고객 테이블에 다음 행이 있다고 하겠습니다.

M001 | 김하늘 | NULL

나중에 고객 메모가 추가됩니다.

M001 | 김하늘 | 장문의 고객 상담 메모...

행의 크기가 증가합니다.

페이지가 이미 꽉 차 있다면 DBMS는 추가 공간을 확보해야 합니다.

제품에 따라 다른 페이지를 사용하거나 행의 일부를 다른 위치에 저장하거나 페이지 재구성과 같은 작업이 발생할 수 있습니다.

따라서 일부 DBMS에서는 페이지를 처음부터 완전히 채우지 않고 일정 공간을 남기는 설정을 제공합니다.


8장. 빈 공간을 많이 남긴다고 항상 좋은 것은 아니다#

여유 공간이 많으면 향후 UPDATE에는 유리할 수 있습니다.

하지만 같은 데이터가 더 많은 페이지를 사용하게 됩니다.

예를 들어 페이지를 거의 꽉 채웠을 때 100개의 페이지가 필요했다고 하겠습니다.

페이지를 상당히 비워 두면 같은 데이터가 120개나 130개의 페이지를 사용할 수도 있습니다.

그러면 전체 스캔에서는 더 많은 페이지를 읽어야 할 수 있습니다.

버퍼 캐시에도 더 많은 페이지가 필요합니다.

즉 다음 두 목표 사이의 균형이 필요합니다.

업데이트를 위한 여유 공간
↕
읽기 효율과 페이지 밀도

무조건 많이 비워 두는 것도, 무조건 꽉 채우는 것도 정답은 아닙니다.


9장. Oracle PCTFREE는 업데이트 공간과 관련된다#

Oracle에서는 PCTFREE라는 저장 속성을 통해 블록에 일정 공간을 남겨 기존 행의 향후 증가에 대비할 수 있습니다.

예를 들어 개념적으로:

PCTFREE = 20

이라면 새 행을 채워가는 과정에서 블록 일부를 향후 행 변경을 위해 남겨 두는 방식으로 이해할 수 있습니다.

다만 다음처럼 해석하면 안 됩니다.

블록의 정확히 20%가 영원히 비어 있다.

공간 관리 방식과 실제 변경 과정은 더 복잡합니다.

PCTFREE는 저장 패턴을 조정하기 위한 설정이지 절대적인 성능 공식이 아닙니다.


10장. PostgreSQL의 fillfactor도 비슷한 고민에서 출발한다#

PostgreSQL에서도 테이블과 인덱스에 fillfactor를 설정할 수 있습니다.

개념적으로는 페이지를 어느 정도까지 채워 둘지를 조정해 이후 변경을 위한 여유를 둘 수 있습니다.

예를 들어 행 업데이트가 많은 테이블과 거의 읽기만 하는 테이블은 적절한 설정이 다를 수 있습니다.

중요한 것은 다음입니다.

fillfactor를 낮춘다
=
무조건 성능이 좋아진다

가 아닙니다.

페이지를 덜 채우면 변경에 여유가 생길 수 있지만 같은 데이터를 위해 더 많은 페이지가 필요합니다.

따라서 읽기 비용과 저장 공간이 늘어날 수도 있습니다.


11장. SQL Server의 인덱스 Fill Factor도 같은 이름만 보고 동일하게 보면 안 된다#

SQL Server에서도 인덱스를 생성하거나 재구성할 때 Fill Factor를 사용할 수 있습니다.

이 역시 향후 인덱스 페이지 변경과 분할을 고려한 설정입니다.

하지만 Oracle PCTFREE, PostgreSQL fillfactor, SQL Server의 Fill Factor가 이름이나 목적이 비슷하다고 해서 세부 동작까지 완전히 같다고 보면 안 됩니다.

DBMS별 저장 구조와 적용 범위를 따로 확인해야 합니다.


12장. 페이지가 부족하면 새로운 공간을 할당해야 한다#

테이블에 계속 데이터가 들어오면 기존 페이지로는 부족해집니다.

그러면 DBMS는 테이블을 위한 새로운 저장 공간을 확보해야 합니다.

Oracle의 논리 저장 구조에서는 여러 블록을 묶어 익스텐트라는 단위로 공간을 할당합니다.

개념적으로 보면 다음과 같습니다.

테이블 세그먼트
├─ 익스텐트 1
│  ├─ 블록
│  ├─ 블록
│  └─ 블록
│
├─ 익스텐트 2
│  ├─ 블록
│  ├─ 블록
│  └─ 블록
│
└─ ...

테이블이 커지면서 필요한 공간이 증가하면 새로운 익스텐트를 할당받게 됩니다.


13장. 익스텐트는 여러 페이지나 블록을 묶은 공간 할당 단위다#

한 페이지가 부족할 때마다 운영체제 수준의 공간을 조금씩 확보하는 것은 비효율적일 수 있습니다.

그래서 DBMS는 여러 페이지 또는 블록을 묶은 단위로 공간을 관리할 수 있습니다.

Oracle에서 익스텐트는 이런 논리적 공간 할당 단위입니다.

이를 다음처럼 이해할 수 있습니다.

블록
→ 데이터 저장과 접근의 작은 단위

익스텐트
→ 여러 블록을 묶은 공간 할당 단위

다만 정확한 크기와 할당 정책은 환경과 설정에 따라 다릅니다.


14장. 세그먼트는 하나의 저장 객체를 위한 공간 집합이다#

테이블 하나에는 여러 익스텐트가 필요할 수 있습니다.

인덱스 역시 별도의 저장 공간이 필요합니다.

Oracle의 개념에서는 특정 데이터베이스 객체가 사용하는 익스텐트의 집합을 세그먼트로 이해할 수 있습니다.

예를 들어:

ORDERS 테이블
→ 테이블 세그먼트

ORDERS_PK 인덱스
→ 인덱스 세그먼트

같은 주문 데이터를 빠르게 찾기 위한 인덱스를 추가하면 테이블 용량만 존재하는 것이 아닙니다.

인덱스도 자신의 저장 공간을 차지합니다.


15장. 인덱스를 만들면 저장 공간은 늘지만 읽기는 줄어들 수 있다#

다음 테이블에 1천만 건이 있다고 하겠습니다.

ORDERS

고객ID 조건으로 자주 검색합니다.

인덱스가 없다면 많은 데이터 페이지를 훑어야 할 수 있습니다.

고객ID 인덱스를 만들면 새로운 인덱스 페이지가 추가됩니다.

즉 저장 공간은 증가합니다.

테이블 페이지
+
인덱스 페이지

하지만 특정 고객의 주문 위치를 빠르게 찾을 수 있어 읽기 작업량은 줄어들 수 있습니다.

이것이 데이터베이스 성능에서 자주 등장하는 교환관계입니다.

추가 저장 공간
+
추가 쓰기 비용
↔
조회 비용 감소

16장. 테이블스페이스는 논리적인 저장 영역이다#

Oracle에서는 세그먼트를 테이블스페이스라는 논리 저장 영역에 배치합니다.

개념적으로 다음과 같습니다.

TABLESPACE
├─ TABLE SEGMENT
├─ INDEX SEGMENT
└─ 기타 세그먼트

그리고 테이블스페이스는 실제 데이터파일을 사용합니다.

TABLESPACE
↓
DATAFILE

따라서 다음 계층을 구분할 수 있습니다.

블록
↓
익스텐트
↓
세그먼트
↓
테이블스페이스
↓
데이터파일

이 계층은 공간의 역할을 이해하는 데 유용합니다.


17장. 논리 저장 영역과 실제 파일은 같은 개념이 아니다#

테이블스페이스와 데이터파일을 같은 것으로 생각하면 안 됩니다.

테이블스페이스는 데이터베이스가 사용하는 논리적인 저장 영역이고 데이터파일은 실제 저장소에 존재하는 파일입니다.

하나의 테이블스페이스가 여러 데이터파일을 사용할 수도 있습니다.

개념적으로 다음과 같이 볼 수 있습니다.

flowchart TD
    TS["Tablespace"] --> F1["Datafile 1"]
    TS --> F2["Datafile 2"]
    TS --> F3["Datafile 3"]

    TS --> S1["Table Segment"]
    TS --> S2["Index Segment"]

    S1 --> E1["Extent"]
    E1 --> B1["Block"]

이 그림은 역할을 이해하기 위한 개념도입니다.


18장. 파일 크기가 곧 실제 데이터 크기는 아니다#

Oracle 데이터파일이 100GB라고 하겠습니다.

그렇다고 테이블 데이터가 정확히 100GB 있다는 뜻은 아닙니다.

파일 안에는 아직 사용하지 않는 공간이 있을 수 있습니다.

인덱스와 기타 객체가 공간을 사용할 수도 있습니다.

또 자동 확장 한도와 현재 파일 크기도 구분해야 합니다.

따라서 저장 공간을 확인할 때는 다음을 나눠 보는 것이 좋습니다.

할당된 공간

사용 중인 공간

파일 내부 여유 공간

자동 확장 가능 한도

스토리지 자체의 남은 공간

이들은 서로 다른 숫자입니다.


19장. Oracle에서 테이블스페이스 공간을 확인하는 예#

영구 데이터파일의 할당 공간과 자유 공간을 개념적으로 확인하는 SQL은 다음처럼 작성할 수 있습니다.

SELECT
    df.tablespace_name,
    ROUND(SUM(df.bytes) / 1024 / 1024, 2) AS allocated_mb,
    ROUND(SUM(NVL(fs.free_bytes, 0)) / 1024 / 1024, 2) AS free_mb,
    ROUND(
        SUM(df.bytes - NVL(fs.free_bytes, 0))
        / 1024 / 1024,
        2
    ) AS used_mb
FROM dba_data_files df
LEFT JOIN (
    SELECT
        file_id,
        SUM(bytes) AS free_bytes
    FROM dba_free_space
    GROUP BY file_id
) fs
    ON fs.file_id = df.file_id
GROUP BY df.tablespace_name
ORDER BY df.tablespace_name;

예를 들어 두 파일이 있다고 하겠습니다.

파일 1
할당 100MiB
자유 30MiB

파일 2
할당 200MiB
자유 20MiB

전체는:

할당 = 300MiB

자유 = 50MiB

사용 추정 = 250MiB

입니다.

여기서 300MiB는 업무 데이터만의 크기가 아니라 파일에 현재 할당된 공간이라는 점을 구분해야 합니다.


20장. 행을 조회할 때 버퍼 캐시가 등장한다#

저장 장치에서 매번 페이지를 읽는 것은 비용이 큽니다.

그래서 DBMS는 자주 사용하는 페이지를 메모리에 보관합니다.

이를 일반적으로 버퍼 캐시 또는 유사한 개념으로 설명할 수 있습니다.

흐름을 단순화하면 다음과 같습니다.

SQL이 페이지 필요
↓
버퍼 캐시 확인
↓
있음?
├─ 예 → 메모리에서 사용
└─ 아니오 → 저장 장치에서 읽어 버퍼에 적재

따라서 같은 SQL이라도 첫 실행과 두 번째 실행의 비용이 다를 수 있습니다.


21장. 논리적 읽기와 물리적 읽기는 다르다#

페이지를 참조했다고 해서 반드시 디스크나 SSD에서 읽었다는 뜻은 아닙니다.

필요한 페이지가 이미 버퍼 캐시에 있을 수 있습니다.

따라서 두 종류의 읽기를 구분해서 보는 것이 중요합니다.

논리적 읽기
→ 메모리의 버퍼 페이지 접근까지 포함한 논리적인 페이지 접근

물리적 읽기
→ 저장 장치에서 페이지를 가져오는 작업

DBMS마다 실제 통계 이름과 정의에는 차이가 있지만 개념적으로 이 구분은 성능 분석에 매우 중요합니다.


22장. 같은 SQL을 두 번째 실행했더니 빨라진 이유#

다음 쿼리를 처음 실행합니다.

SELECT *
FROM orders
WHERE order_id = 'O001';

필요한 인덱스 페이지와 데이터 페이지가 메모리에 없다고 가정합니다.

저장 장치
↓
버퍼 캐시
↓
조회

첫 실행은 물리적 I/O가 포함될 수 있습니다.

곧바로 다시 같은 SQL을 실행합니다.

필요한 페이지가 버퍼에 남아 있다면:

버퍼 캐시
↓
조회

만으로 처리될 수 있습니다.

두 번째 실행이 훨씬 빠를 수 있습니다.

하지만 데이터 구조가 바뀐 것은 아닙니다.

인덱스가 새로 생긴 것도 아닙니다.

단지 필요한 페이지가 메모리에 있었기 때문일 수 있습니다.


23장. 캐시 효과를 인덱스 효과로 착각하면 안 된다#

인덱스 생성 전 쿼리를 한 번 실행했습니다.

첫 실행:

300ms

인덱스를 만든 뒤 같은 쿼리를 다시 실행했습니다.

두 번째 실행:

50ms

이 숫자만 보면 인덱스 덕분에 6배 빨라졌다고 말하고 싶어집니다.

하지만 두 실행의 캐시 상태가 다를 수 있습니다.

첫 번째는 필요한 데이터가 메모리에 없었고 두 번째는 이미 캐시되어 있었을 수도 있습니다.

따라서 성능 테스트에서는 다음 조건을 기록해야 합니다.

실행 계획

실제 읽은 페이지 수

캐시 상태

반환 행 수

반복 실행 횟수

동시 부하

시간 하나만 비교해서는 정확한 원인을 알기 어렵습니다.


24장. 운영 서버의 캐시를 함부로 비우면 안 된다#

성능 테스트를 위해 다음과 같이 생각할 수 있습니다.

캐시를 비우고 다시 해보자.

하지만 운영 DBMS를 재시작하거나 시스템 캐시를 강제로 비우는 것은 다른 사용자와 전체 시스템에 큰 영향을 줄 수 있습니다.

따라서 이런 실험은 별도의 테스트 환경에서 수행해야 합니다.

운영 환경에서는 DBMS가 제공하는 통계와 실행 계획, I/O 지표를 이용해 분석하는 것이 안전합니다.


25장. 인덱스로 한 행을 찾는 과정도 여러 페이지를 거칠 수 있다#

B트리 계열 인덱스를 개념적으로 단순화하면 다음과 같은 구조를 생각할 수 있습니다.

Root Page
   ↓
Branch Page
   ↓
Leaf Page
   ↓
Table Page

주문ID 하나를 찾는다고 하겠습니다.

인덱스 루트 페이지를 읽습니다.

중간 페이지를 거칩니다.

리프 페이지에서 행 위치를 찾습니다.

필요한 실제 데이터가 인덱스에 모두 없다면 테이블 페이지를 추가로 읽습니다.

즉 결과 한 행을 얻기 위해 여러 페이지를 접근할 수 있습니다.


26장. 커버링 가능한 조회는 테이블 페이지 접근을 줄일 수 있다#

다음 인덱스에 조회에 필요한 모든 값이 포함되어 있다고 가정하겠습니다.

(customer_id, order_date, status)

그리고 SQL이 다음 값만 필요합니다.

SELECT
    order_date,
    status
FROM orders
WHERE customer_id = 'M001';

DBMS와 실행 조건에 따라 인덱스만으로 필요한 결과를 얻을 수 있는 경우가 있습니다.

그렇다면 테이블 본문 페이지까지 접근할 필요가 줄어들 수 있습니다.

이런 접근은 저장 공간과 쓰기 비용을 더 사용하지만 특정 조회에서는 페이지 접근을 줄일 수 있습니다.


27장. 큰 값은 행과 다른 저장 경로를 사용할 수도 있다#

한 행에 매우 큰 본문이나 JSON, BLOB 같은 값이 있다고 하겠습니다.

게시글ID
제목
본문 5MB

모든 값을 동일한 작은 페이지에 그대로 넣기 어려울 수 있습니다.

DBMS는 제품별로 큰 값을 별도 영역이나 다른 저장 구조에 둘 수 있습니다.

따라서 다음 SQL은:

SELECT post_id, title
FROM post
WHERE post_id = 100;

과:

SELECT *
FROM post
WHERE post_id = 100;

가 동일한 저장 접근 비용을 가진다고 단정할 수 없습니다.

필요한 열에 따라 추가 저장 영역을 읽어야 할 수도 있습니다.


28장. 결과 행 수와 읽은 페이지 수는 다른 지표다#

다음 두 조회가 모두 100행을 반환한다고 하겠습니다.

조회 A는 100행이 몇 개의 가까운 페이지에 모여 있습니다.

조회 B는 100행이 테이블 전체에 흩어져 있습니다.

결과 행 수는 같습니다.

100행

하지만 실제 접근 페이지 수는 크게 다를 수 있습니다.

조회 A
→ 5개 페이지

조회 B
→ 90개 페이지

이런 차이는 인덱스와 행 배치, 클러스터링 정도 등에 따라 나타날 수 있습니다.

따라서 성능에서는 반환 행 수뿐 아니라 그 행을 찾기 위해 얼마나 많은 저장 단위를 읽었는가가 중요합니다.


29장. 전체 스캔이 항상 나쁜 것은 아니다#

인덱스를 사용하는 것이 항상 가장 적은 페이지를 읽는 것은 아닙니다.

테이블 대부분의 행을 읽어야 한다면 인덱스를 따라가며 테이블 페이지를 여러 번 방문하는 것보다 순차적으로 테이블을 읽는 편이 효율적일 수 있습니다.

예를 들어 1천만 행 중 900만 행을 읽는다고 하겠습니다.

이때 DBMS가 전체 스캔을 선택했다고 해서 무조건 잘못된 실행 계획이라고 말할 수 없습니다.

비용 기반 옵티마이저는 데이터 분포와 통계 등을 고려해 여러 접근 경로를 비교합니다.


30장. 저장 구조와 실행 계획은 함께 봐야 한다#

다음과 같은 문제가 있다고 하겠습니다.

주문ID 조회가 느리다.

저장 구조만 보고 다음처럼 결론 내리면 안 됩니다.

페이지가 너무 크다.

또는:

테이블이 너무 크다.

먼저 다음을 함께 확인해야 합니다.

어떤 실행 계획을 사용했는가

인덱스를 사용했는가

몇 행을 예상했는가

실제로 몇 행이 나왔는가

얼마나 많은 페이지를 읽었는가

버퍼 캐시에서 얼마나 처리됐는가

락이나 로그 대기가 있었는가

저장 구조는 성능 분석의 한 축입니다.


31장. 테이블 용량이 커졌다고 곧바로 조회가 느려지는 것은 아니다#

테이블이 1GB에서 100GB가 됐다고 하겠습니다.

무조건 모든 조회가 100배 느려질까요?

그렇지 않습니다.

기본키 인덱스로 한 건을 찾는 조회라면 전체 테이블을 읽지 않을 수 있습니다.

반대로 1GB 테이블이라도 잘못된 조건으로 매번 전체 스캔하고 있다면 느릴 수 있습니다.

따라서 다음 두 문장은 다릅니다.

테이블이 크다.

해당 조회가 많은 데이터를 읽는다.

성능에서 더 중요한 것은 두 번째입니다.


32장. SQLite로 페이지 수 변화를 관찰해 볼 수 있다#

SQLite를 이용하면 작은 실험 환경에서 페이지 단위 저장을 관찰할 수 있습니다.

예를 들어 다음처럼 데이터를 만듭니다.

CREATE TABLE event (
    id INTEGER PRIMARY KEY,
    category TEXT,
    payload TEXT
);

1만 건을 넣습니다.

WITH RECURSIVE seq(x) AS (
    SELECT 1
    UNION ALL
    SELECT x + 1
    FROM seq
    WHERE x < 10000
)
INSERT INTO event
SELECT
    x,
    'C' || (x % 20),
    printf('%0200d', x)
FROM seq;

페이지 크기를 확인합니다.

PRAGMA page_size;

현재 파일 페이지 수를 확인합니다.

PRAGMA page_count;

33장. 인덱스를 만든 뒤 페이지 수를 다시 확인해 보자#

카테고리 검색용 인덱스를 만듭니다.

CREATE INDEX event_category_idx
ON event(category);

다시 확인합니다.

PRAGMA page_count;

일반적으로 인덱스 자체가 페이지를 사용하므로 데이터베이스 파일의 전체 페이지 수가 늘어날 수 있습니다.

그러나 다음 검색에서는 접근 경로가 달라질 수 있습니다.

EXPLAIN QUERY PLAN
SELECT id
FROM event
WHERE category = 'C7';

여기서 관찰해야 할 것은 두 가지입니다.

저장 공간
→ 늘어날 수 있음

검색 경로
→ 더 선택적으로 바뀔 수 있음

인덱스는 공간과 쓰기 비용을 사용하는 대신 읽기 비용을 줄일 수 있는 구조입니다.


34장. page_count만으로 실제 I/O를 알 수는 없다#

SQLite의 page_count는 데이터베이스 파일이 사용하는 전체 페이지 수를 관찰하는 데 도움을 줍니다.

하지만 다음을 알려주는 값은 아닙니다.

이 SELECT가 실제로 몇 페이지를 디스크에서 읽었는가?

파일 페이지 수와 조회의 실제 물리 I/O는 다른 지표입니다.

마찬가지로 실행 계획에서 인덱스 검색이 보인다고 해서 정확히 몇 번의 저장 장치 읽기가 발생했다고 알 수 있는 것도 아닙니다.

성능 지표는 각자의 의미를 구분해서 사용해야 합니다.


35장. 저장 용량도 여러 종류로 나누어 봐야 한다#

데이터베이스 서버에서 다음과 같은 숫자를 볼 수 있습니다.

테이블 크기
인덱스 크기
데이터파일 크기
로그 크기
임시 공간
백업 크기
스토리지 사용량

모두 데이터베이스 용량과 관련 있지만 같은 숫자는 아닙니다.

예를 들어 데이터파일이 500GB라고 해서 테이블 데이터가 500GB인 것은 아닙니다.

인덱스가 200GB라고 해서 모두 메모리에 올라오는 것도 아닙니다.

각 지표가 무엇을 측정하는지 확인해야 합니다.


36장. 테이블보다 인덱스가 더 커질 수도 있다#

테이블에는 필요한 업무 열만 저장되어 있지만 여러 인덱스를 추가하면 각 인덱스가 자신의 키와 행 위치 정보를 저장합니다.

다음과 같은 인덱스가 있다고 하겠습니다.

고객ID 인덱스

주문일 인덱스

상태 인덱스

고객ID + 주문일 복합 인덱스

상태 + 주문일 복합 인덱스

조회에는 도움이 될 수 있습니다.

하지만 인덱스 저장 공간과 쓰기 비용도 함께 증가합니다.

INSERT 한 번에 테이블만 수정하는 것이 아니라 여러 인덱스 페이지도 변경해야 할 수 있습니다.


37장. UPDATE가 느릴 때는 행 크기만 볼 것이 아니다#

UPDATE가 느리다고 하겠습니다.

원인은 여러 가지일 수 있습니다.

  • 변경 열 때문에 행이 커짐
  • 페이지 여유 공간 부족
  • 여러 인덱스 갱신
  • 로그 기록
  • 락 대기
  • 동시 트랜잭션
  • 스토리지 지연
  • 버전 관리 비용

따라서 다음처럼 단순화하면 안 됩니다.

UPDATE가 느리니까 페이지가 가득 찼다.

저장 구조와 트랜잭션, 인덱스, 동시성을 함께 봐야 합니다.


38장. 변경된 페이지가 메모리에 있다고 데이터가 안전하게 저장된 것은 아니다#

UPDATE가 발생하면 변경된 페이지가 메모리 버퍼에서 수정될 수 있습니다.

그렇다면 COMMIT 순간마다 해당 데이터 페이지 전체를 즉시 디스크에 써야 할까요?

현대 DBMS는 일반적으로 로그 기반 복구 구조를 사용하며, 데이터 페이지의 쓰기 시점과 트랜잭션의 내구성을 분리할 수 있습니다.

개념적으로 다음 흐름을 볼 수 있습니다.

행 변경
↓
버퍼 페이지 변경
↓
로그 기록
↓
COMMIT
↓
변경된 데이터 페이지는 적절한 시점에 저장

구체적인 WAL이나 redo 동작은 DBMS에 따라 다릅니다.

중요한 점은 버퍼 캐시는 조회뿐 아니라 변경된 페이지 관리에도 사용된다는 것입니다.


39장. 버퍼 캐시와 로그는 역할이 다르다#

버퍼 캐시는 데이터 페이지를 메모리에서 빠르게 읽고 수정하기 위한 영역입니다.

로그는 트랜잭션 변경을 복구할 수 있도록 기록하는 데 중요한 역할을 합니다.

둘을 같은 것으로 생각하면 안 됩니다.

버퍼 캐시
→ 데이터 페이지 접근과 재사용

로그
→ 변경 기록과 복구

데이터 페이지가 아직 저장 장치에 기록되지 않았더라도 필요한 로그가 안전하게 보존되어 있다면 장애 복구 과정에서 변경을 재현할 수 있는 구조를 사용할 수 있습니다.


40장. 한 행 조회를 전체 흐름으로 따라가 보기#

다음 SQL을 실행합니다.

SELECT
    order_id,
    customer_id,
    status
FROM orders
WHERE order_id = 'O10001';

개념적 흐름은 다음과 같습니다.

flowchart TD
    SQL["SQL 실행"] --> PLAN["실행 계획 선택"]
    PLAN --> IDX["인덱스 탐색"]
    IDX --> BC{"인덱스 페이지가\n버퍼에 있는가?"}
    BC -->|예| LEAF["인덱스 리프 확인"]
    BC -->|아니오| IO1["저장 장치에서 페이지 읽기"]
    IO1 --> LEAF
    LEAF --> ROW["테이블 행 위치 확인"]
    ROW --> BC2{"데이터 페이지가\n버퍼에 있는가?"}
    BC2 -->|예| DATA["행 읽기"]
    BC2 -->|아니오| IO2["데이터 페이지 읽기"]
    IO2 --> DATA
    DATA --> RESULT["결과 반환"]

실제 제품의 내부 구현은 더 복잡하지만 성능을 이해하는 개념적 흐름으로 유용합니다.


41장. 인덱스가 있어도 테이블 페이지 접근이 많을 수 있다#

다음 조회가 있다고 하겠습니다.

SELECT *
FROM orders
WHERE status = '완료';

완료 상태가 전체 주문의 90%라고 가정합니다.

상태 인덱스를 사용하더라도 거의 모든 행 위치를 찾아야 합니다.

그리고 SELECT *이므로 테이블 본문도 대량으로 읽어야 할 수 있습니다.

이런 경우 인덱스가 존재한다는 사실만으로 빠르다고 말할 수 없습니다.

선택도와 반환 행 수가 중요합니다.


42장. 버퍼 캐시가 크다고 모든 조회가 빠른 것은 아니다#

메모리가 크면 더 많은 페이지를 캐시할 수 있습니다.

그러나 다음과 같은 경우에는 여전히 느릴 수 있습니다.

잘못된 실행 계획

필요 이상으로 많은 행 처리

비효율적인 조인

락 대기

정렬이나 해시 작업

메모리 부족으로 임시 파일 사용

스토리지 지연

CPU 병목

따라서 캐시 적중률 하나만 보고 데이터베이스 성능을 판단해서도 안 됩니다.


43장. 페이지 수와 캐시 효율은 연결되어 있다#

같은 100GB 데이터라도 자주 사용하는 부분이 1GB라면 그 영역이 메모리에 잘 남을 수 있습니다.

반대로 매 요청마다 100GB 전체를 무작위로 읽는다면 큰 버퍼 캐시가 있어도 적중률이 낮을 수 있습니다.

중요한 것은 데이터베이스 전체 크기보다 활성 작업 집합입니다.

즉 실제로 자주 접근하는 페이지 집합이 메모리 크기와 어떻게 맞는지를 보는 것이 중요합니다.


44장. 페이지 접근을 줄이는 대표적인 방법#

페이지 읽기가 실제 병목으로 확인됐다면 여러 방법을 검토할 수 있습니다.

적절한 인덱스 사용

조회 범위 축소

불필요한 SELECT * 제거

파티션 프루닝 활용

커버링 가능한 인덱스 검토

큰 열 분리 검토

데이터 배치와 접근 패턴 개선

캐시 활용

하지만 어떤 방법이 효과적인지는 실제 실행 계획과 I/O 통계를 보고 판단해야 합니다.


45장. 물리 저장 구조를 이해하면 용량 문제도 다르게 보인다#

서버 디스크가 갑자기 80%까지 찼다고 하겠습니다.

단순히 다음처럼 결론 내리면 안 됩니다.

회원 데이터가 많이 늘었다.

다른 원인도 있을 수 있습니다.

새 인덱스 생성

로그 증가

임시 공간 증가

대량 정렬

테이블 데이터 증가

삭제 후 공간 미회수

백업 파일 누적

테이블스페이스 자동 확장

물리 저장 구조를 이해하면 파일 증가를 객체별로 나누어 조사할 수 있습니다.


46장. 논리 용량과 실제 스토리지 비용도 다를 수 있다#

클라우드 환경에서는 다음 숫자가 모두 다를 수 있습니다.

DBMS 내부 할당량

실제 사용 데이터량

프로비저닝 스토리지

백업 저장량

스냅샷 저장량

복제본 저장량

따라서 DBMS에서 500GB가 보인다는 이유만으로 청구되는 스토리지도 정확히 500GB라고 단정해서는 안 됩니다.

운영에서는 데이터베이스 내부 지표와 인프라 스토리지 지표를 구분해야 합니다.


47장. 저장 구조를 성능 분석에 연결하는 질문#

조회가 느릴 때 다음 질문을 순서대로 던져볼 수 있습니다.

결과는 몇 행인가?#

1행?
100행?
100만 행?

실제로 얼마나 많은 행을 검사했는가?#

결과는 1행이어도 수백만 행을 읽었을 수 있습니다.

어떤 접근 경로를 사용했는가?#

전체 스캔

인덱스 검색

인덱스 범위 검색

얼마나 많은 페이지를 읽었는가?#

논리적 읽기와 물리적 읽기를 구분합니다.

버퍼에서 얼마나 재사용됐는가?#

캐시 상태를 확인합니다.

인덱스를 사용한 뒤 테이블 페이지 접근은 얼마나 발생했는가?#

인덱스만으로 끝났는지 테이블 접근이 추가됐는지 확인합니다.

이 질문을 이용하면 단순한 “쿼리가 느리다”에서 실제 원인으로 접근할 수 있습니다.


48장. 저장 공간을 분석할 때도 같은 원칙이 필요하다#

공간 사용량이 커졌다면 다음을 구분합니다.

테이블 증가

인덱스 증가

로그 증가

임시 영역 증가

파일 할당 증가

실제 사용 공간 증가

그리고 다음 질문을 추가합니다.

삭제한 공간이 재사용 가능한가?

파일 자체 크기도 줄어들었는가?

자동 확장 한도는 얼마인가?

현재 스토리지 여유 공간은 얼마인가?

같은 “용량”이라는 단어라도 서로 다른 지표를 의미할 수 있습니다.


49장. 페이지·익스텐트·세그먼트의 관계를 정리하면#

Oracle의 논리 저장 구조를 기준으로 개념을 정리하면 다음과 같습니다.

단위 역할
블록 데이터를 저장하고 접근하는 기본 단위
익스텐트 여러 블록을 묶은 공간 할당 단위
세그먼트 객체가 사용하는 익스텐트의 집합
테이블스페이스 세그먼트를 배치하는 논리 저장 영역
데이터파일 테이블스페이스가 사용하는 실제 파일

흐름으로 보면 다음과 같습니다.

flowchart TD
    DF["Datafile"] --> TS["Tablespace"]
    TS --> S["Segment"]
    S --> E1["Extent"]
    S --> E2["Extent"]
    E1 --> B1["Block"]
    E1 --> B2["Block"]
    E2 --> B3["Block"]
    E2 --> B4["Block"]

물리적으로 데이터파일이 가장 아래 저장 매체에 있지만, 그림에서는 관리 계층을 이해하기 쉽게 표현한 것입니다.


50장. 저장 구조를 외우는 것보다 역할을 구분하는 것이 중요하다#

다음처럼 단순히 순서를 외우는 것만으로는 실무에서 큰 도움이 되지 않습니다.

블록 → 익스텐트 → 세그먼트 → 테이블스페이스

각 단위가 어떤 질문에 답하는지를 함께 기억하는 편이 좋습니다.

블록
→ 실제 데이터를 어떤 단위로 읽고 관리하는가?

익스텐트
→ 추가 공간을 어떤 단위로 할당하는가?

세그먼트
→ 이 테이블이나 인덱스가 어떤 공간을 사용하는가?

테이블스페이스
→ 객체들을 어느 논리 영역에 배치하는가?

데이터파일
→ 실제 파일은 어디에 존재하는가?

51장. 핵심 정리#

데이터베이스에서 사용자가 보는 행과 내부 저장 구조는 다릅니다.

한 행을 조회했다고 해서 저장 장치에서 그 행 몇 바이트만 읽는 것은 아닙니다.

데이터베이스는 보통 페이지 또는 블록 단위로 데이터를 읽고 관리합니다.

행
↓
페이지 또는 블록
↓
공간 할당 단위
↓
객체 저장 영역
↓
논리 저장 영역
↓
파일

페이지 안에는 실제 업무 데이터뿐 아니라 페이지 헤더, 행 위치 관리 정보, 여유 공간 등도 존재합니다.

따라서 단순히 컬럼 크기와 행 수를 곱한 값이 실제 저장량과 정확히 일치하지 않습니다.

또한 인덱스를 추가하면 저장 공간과 쓰기 비용은 증가하지만 특정 조회에서 읽어야 하는 페이지 수를 줄일 수 있습니다.

성능을 분석할 때는 다음 세 가지를 반드시 구분해야 합니다.

결과 행 수

논리적 페이지 접근량

실제 물리 I/O

결과가 한 행이라고 읽기도 한 번인 것은 아닙니다.

논리적 페이지 접근이 발생했다고 모두 디스크에서 읽은 것도 아닙니다.

버퍼 캐시에 필요한 페이지가 이미 있다면 저장 장치 접근 없이 메모리에서 처리될 수 있습니다.

그래서 같은 SQL도 첫 번째 실행과 두 번째 실행의 시간이 다를 수 있습니다.

이 차이를 인덱스 효과로 착각하지 않으려면 실행 계획과 캐시 상태, 페이지 접근량을 함께 확인해야 합니다.

저장 용량에서도 마찬가지입니다.

데이터파일 크기
≠
실제 테이블 데이터 크기

테이블 크기
≠
인덱스를 포함한 전체 데이터베이스 크기

할당 공간
≠
실제 사용 공간

결국 데이터베이스 저장 구조를 이해하는 목적은 용어를 외우는 데 있지 않습니다.

SQL 한 문장이 실제로 어떤 페이지를 찾고, 그 페이지가 어디에 있으며, 메모리에서 재사용되는지 아니면 저장 장치에서 새로 읽히는지를 설명할 수 있어야 합니다.

이 흐름을 이해하면 “테이블이 크다”, “인덱스가 있다”, “메모리가 많다” 같은 막연한 설명을 넘어 실제 페이지 접근과 I/O를 기준으로 데이터베이스의 저장 공간과 성능을 분석할 수 있습니다.

이 페이지의 목차