Devin.KR

DB 아키텍처 한눈에 - 인스턴스·버퍼 캐시·로그와 I/O 흐름

개발자KR 조회 9

이 장에서 배우는 것

SQL 튜닝을 공부하다 보면 "실행계획을 읽어라", "인덱스를 잘 타게 해라"는 말을 먼저 듣게 된다. 하지만 실행계획에 나오는 Rows, Cost, 버퍼 읽기 같은 숫자는 모두 DB 인스턴스가 메모리와 디스크를 오가며 일하는 방식에서 나온다. 이 장에서는 오라클 인스턴스가 어떤 프로세스와 메모리 구조로 이루어져 있는지, SELECT 한 번과 COMMIT 한 번이 각각 어떤 경로를 거쳐 디스크까지 가는지를 그림과 코드로 확인한다. 이후 장에서 다룰 논리 읽기·물리 읽기, 실행계획, 락(Lock)은 모두 이 장에서 세운 그림 위에 놓인다.

  • 서버 프로세스와 SGA(공유 메모리 영역, System Global Area)를 구분해서 설명할 수 있다
  • 버퍼 캐시·로그 버퍼·공유 풀이 각각 어떤 역할을 맡는지 안다
  • COMMIT 시점에 실제로 디스크에 기록되는 것과, 나중에 기록되는 것을 구분할 수 있다
  • SELECT 한 번이 버퍼 캐시를 거쳐 데이터 파일까지 가는 경로를 그릴 수 있다
  • 인스턴스 장애 후에도 커밋된 데이터가 사라지지 않는 이유를 설명할 수 있다

문제 상황

온라인 서점의 주문 시스템을 맡고 있는 개발자가 회원별 주문 내역을 조회하는 쿼리를 테스트하고 있다. 같은 조건으로 같은 쿼리를 연달아 두 번 실행했는데, 첫 번째는 300밀리초가 걸렸고 두 번째는 3밀리초 만에 끝났다. 쿼리도 데이터도 바뀐 게 없는데 왜 이런 차이가 나는지 설명하지 못한 채 "서버가 워밍업되나 보다"라고 넘어갔다.

같은 팀의 운영 담당자는 다른 문제로 당황했다. 배치 프로그램이 주문 상태를 변경하고 COMMIT까지 정상적으로 끝냈다는 로그를 남겼는데, 그 직후 데이터 파일의 수정 시각을 확인해 보니 변화가 없었다. "커밋했다면서 왜 파일이 그대로냐"는 질문에 누구도 바로 답하지 못했다. 두 상황 모두 원인은 같다. 인스턴스가 메모리와 디스크를 어떤 순서로 쓰는지 모르면 정상 동작을 장애로 오해하게 된다.

이 장의 예제는 온라인 서점 도메인을 그대로 쓴다. 실제 운영 규모는 주문 1천만 건 수준이며, 아래 표가 이 책 전체에서 쓸 기본 스키마다.

이 장에서 쓰는 온라인 서점 스키마
테이블주요 컬럼인덱스비고
MEMBERSMEMBER_ID(PK), MEMBER_NAME, EMAILPK(MEMBER_ID)회원 약 50만 명 가정
BOOKSBOOK_ID(PK), TITLE, CATEGORY, PRICEPK(BOOK_ID)도서 약 8만 종 가정
ORDERSORDER_ID(PK), MEMBER_ID, ORDER_DATE, STATUSPK(ORDER_ID), IX_ORDERS_MEMBER(MEMBER_ID)약 1천만 건
ORDER_ITEMSORDER_ID, BOOK_ID, QUANTITYPK(ORDER_ID, BOOK_ID), IX_ORDER_ITEMS_BOOK(BOOK_ID)주문당 평균 2권
REVIEWSREVIEW_ID(PK), BOOK_ID, MEMBER_ID, RATINGPK(REVIEW_ID), IX_REVIEWS_BOOK(BOOK_ID)약 300만 건

인스턴스 메모리 구조와 프로세스

오라클에서 "인스턴스"는 메모리 구조와 그 메모리를 다루는 프로세스들의 집합을 가리킨다. 데이터 파일 같은 물리적인 저장소는 "데이터베이스"라고 따로 부르지만, 실무에서는 둘을 묶어서 그냥 "DB"라고 부르는 경우가 많다. 사용자가 SQL을 실행하면 그 요청을 받는 쪽은 서버 프로세스이고, 서버 프로세스가 실제로 데이터를 읽고 쓰는 작업장이 바로 SGA다.

SGA를 이루는 세 가지

SGA는 인스턴스에 접속한 모든 세션이 함께 쓰는 공유 메모리다. 이 장에서 눈여겨볼 구성요소는 세 가지다.

  • 버퍼 캐시: 데이터 파일에서 읽어 온 블록을 담아 두는 공간이다. 같은 블록을 다시 읽을 때 디스크까지 가지 않고 메모리에서 바로 돌려주기 위해 존재한다.
  • 로그 버퍼: INSERT·UPDATE·DELETE로 바뀐 내용을 REDO(다시 실행 가능한 변경 이력) 형태로 임시로 담아 두는 공간이다. 곧 LGWR이 이 내용을 로그 파일에 옮긴다.
  • 공유 풀: 파싱된 SQL과 실행계획, 데이터 딕셔너리 정보를 캐시한다. 다음 장에서 다룰 SQL 처리 과정은 이 공유 풀을 중심으로 벌어진다.

서버 프로세스와 백그라운드 프로세스

사용자가 접속하면 그 세션을 전담하는 서버 프로세스가 하나 뜬다. 서버 프로세스는 SQL을 해석하고, 필요한 블록을 버퍼 캐시에서 찾거나 없으면 직접 데이터 파일을 읽어 캐시에 올린다. 반면 디스크에 실제로 내용을 내려쓰는 일은 서버 프로세스가 아니라 별도의 백그라운드 프로세스가 맡는다. 이 장에서 볼 두 프로세스는 다음과 같다.

  • DBWn(Database Writer): 버퍼 캐시에서 바뀐 블록(더티 블록)을 골라 데이터 파일에 내려쓴다. 체크포인트 시점이나 버퍼 캐시가 부족할 때 등 비동기적으로 동작한다.
  • LGWR(Log Writer): 로그 버퍼의 내용을 REDO 로그 파일에 내려쓴다. COMMIT이 호출되면 반드시, 그리고 곧바로 동작한다.

그림으로 정리하면 다음과 같다.

버퍼 캐시는 DBWn이, 로그 버퍼는 LGWR이 서로 다른 시점에 디스크로 내려쓴다

오라클과 MySQL의 차이

용어는 다르지만 구조는 거의 같다. MySQL 8(InnoDB 기준)과 비교하면 아래와 같다.

Oracle 19c와 MySQL 8(InnoDB)의 메모리·로그 구조 비교
구분Oracle 19cMySQL 8(InnoDB)비고
데이터 캐시버퍼 캐시(SGA 내부)InnoDB 버퍼 풀이름은 달라도 블록·페이지 캐시라는 역할은 같다
변경 기록 캐시로그 버퍼redo log buffer둘 다 커밋 시 로그 파일에 먼저 기록한다
디스크 반영 프로세스DBWn페이지 클리너 스레드더티 블록·페이지를 비동기로 내려쓴다
로그 기록 프로세스LGWR로그 기록 스레드커밋 시 즉시 플러시(선행 기록)한다

구조를 더 깊이 확인하려면 공식 문서를 참고할 수 있다. Oracle Database Concepts, 19c

읽기 한 번이 디스크까지 가는 경로

서버 프로세스가 SELECT를 처리할 때 가장 먼저 하는 일은 필요한 블록이 버퍼 캐시에 이미 있는지 확인하는 것이다. 있으면(캐시 히트) 메모리에서 바로 돌려주고 끝난다. 이 경우를 논리 읽기(logical read)라고 부른다. 없으면(캐시 미스) 데이터 파일에서 블록을 읽어 오는데, 이 디스크 접근을 물리 읽기(physical read)라고 부른다. 물리 읽기로 가져온 블록은 버려지지 않고 버퍼 캐시에 적재되므로, 같은 블록을 다시 요청하면 다음번에는 캐시에서 바로 찾는다.

앞서 문제 상황에서 같은 쿼리를 두 번 실행했을 때 두 번째가 훨씬 빨랐던 이유가 여기 있다. 첫 실행에서 물리 읽기로 캐시에 올라간 블록을 두 번째 실행이 그대로 재사용한 것이다. 논리 읽기와 물리 읽기의 비용 차이, 그리고 이 비용을 실행계획에서 어떻게 읽는지는 블록 단위 I/O를 다루는 장에서 더 깊이 살펴본다. 여기서는 "캐시에 있으면 짧게 끝나고, 없으면 디스크까지 간다"는 경로만 정확히 잡아 두면 된다.

버퍼 캐시에 블록이 있으면 논리 읽기로 끝나고 없으면 물리 읽기 후 캐시에 적재된다

커밋 시 일어나는 것

UPDATE나 INSERT 같은 DML을 실행하면 서버 프로세스는 버퍼 캐시에 있는 블록을 바로 바꾸고, 그 변경 내용을 로그 버퍼에도 함께 적는다. 이 시점까지는 아직 아무것도 디스크에 반영되지 않는다. COMMIT을 호출하면 LGWR이 로그 버퍼의 내용을 REDO 로그 파일에 즉시 기록하고, 이 기록이 끝나야 COMMIT이 사용자에게 성공을 돌려준다. 이것을 선행 기록(write-ahead logging)이라고 부른다. 변경된 데이터 블록 자체는 이 시점에 데이터 파일에 반영되지 않는다. 여전히 버퍼 캐시에 더티 블록으로 남아 있다가, DBWn이 체크포인트나 버퍼 부족 등의 조건에서 비동기로 데이터 파일에 내려쓴다.

이 순서를 알면 두 가지가 자연스럽게 풀린다. 첫째, 커밋 직후 데이터 파일 수정 시각이 그대로인 것은 정상이다. 커밋이 보장하는 것은 REDO 로그 파일에 대한 기록이지 데이터 파일 반영이 아니다. 둘째, 인스턴스가 비정상 종료된 뒤에도 커밋된 데이터는 사라지지 않는다. 재시작 시 REDO 로그를 읽어 커밋된 변경을 다시 적용하는 크래시 복구가 자동으로 일어나기 때문이다.

커밋은 REDO 로그 파일 반영을 보장할 뿐 데이터 파일 반영은 이후 DBWn이 처리한다

완성 코드

아래 스크립트는 온라인 서점 스키마를 만들고, 버퍼 캐시 히트·미스와 커밋 시 REDO 기록을 세션 통계(v$mystat)로 직접 확인한다. SQL*Plus에서 순서대로 실행한다.

01_schema.sql

CREATE TABLE members (
  member_id     NUMBER        NOT NULL,
  member_name   VARCHAR2(50)  NOT NULL,
  email         VARCHAR2(100) NOT NULL,
  joined_date   DATE          NOT NULL,
  CONSTRAINT pk_members PRIMARY KEY (member_id)
);

CREATE TABLE books (
  book_id         NUMBER        NOT NULL,
  title           VARCHAR2(200) NOT NULL,
  category        VARCHAR2(30),
  price           NUMBER(8,0)   NOT NULL,
  published_date  DATE,
  CONSTRAINT pk_books PRIMARY KEY (book_id)
);

CREATE TABLE orders (
  order_id    NUMBER        NOT NULL,
  member_id   NUMBER        NOT NULL,
  order_date  DATE          NOT NULL,
  status      VARCHAR2(10)  NOT NULL,
  CONSTRAINT pk_orders PRIMARY KEY (order_id),
  CONSTRAINT fk_orders_member FOREIGN KEY (member_id)
    REFERENCES members(member_id)
);

CREATE INDEX ix_orders_member ON orders(member_id);

CREATE TABLE order_items (
  order_id     NUMBER      NOT NULL,
  book_id      NUMBER      NOT NULL,
  quantity     NUMBER(3,0) NOT NULL,
  order_price  NUMBER(8,0) NOT NULL,
  CONSTRAINT pk_order_items PRIMARY KEY (order_id, book_id),
  CONSTRAINT fk_items_order FOREIGN KEY (order_id)
    REFERENCES orders(order_id),
  CONSTRAINT fk_items_book FOREIGN KEY (book_id)
    REFERENCES books(book_id)
);

CREATE INDEX ix_order_items_book ON order_items(book_id);

CREATE TABLE reviews (
  review_id    NUMBER      NOT NULL,
  book_id      NUMBER      NOT NULL,
  member_id    NUMBER      NOT NULL,
  rating       NUMBER(1,0) NOT NULL,
  review_date  DATE        NOT NULL,
  CONSTRAINT pk_reviews PRIMARY KEY (review_id),
  CONSTRAINT fk_reviews_book FOREIGN KEY (book_id)
    REFERENCES books(book_id),
  CONSTRAINT fk_reviews_member FOREIGN KEY (member_id)
    REFERENCES members(member_id)
);

CREATE INDEX ix_reviews_book ON reviews(book_id);

INSERT INTO members VALUES (1001, '김도윤', 'dy.kim@example.com', DATE '2023-03-02');
INSERT INTO members VALUES (1002, '이서연', 'sy.lee@example.com', DATE '2023-05-11');
INSERT INTO members VALUES (1003, '박지훈', 'jh.park@example.com', DATE '2024-01-20');
INSERT INTO members VALUES (1004, '최민아', 'ma.choi@example.com', DATE '2024-02-14');

INSERT INTO books VALUES (2001, 'SQL 실행계획 읽는 법', 'IT', 28000, DATE '2022-09-01');
INSERT INTO books VALUES (2002, '데이터베이스 인덱스 설계', 'IT', 26000, DATE '2023-04-15');
INSERT INTO books VALUES (2003, '소설 어느 여름날', '소설', 15000, DATE '2021-07-10');

INSERT INTO orders VALUES (3001, 1004, DATE '2026-08-01', 'DONE');
INSERT INTO orders VALUES (3002, 1004, DATE '2026-08-15', 'DONE');
INSERT INTO orders VALUES (3003, 1002, DATE '2026-09-02', 'DONE');
INSERT INTO orders VALUES (3004, 1004, DATE '2026-09-20', 'PAID');

INSERT INTO order_items VALUES (3001, 2001, 1, 28000);
INSERT INTO order_items VALUES (3002, 2002, 2, 26000);
INSERT INTO order_items VALUES (3003, 2003, 1, 15000);
INSERT INTO order_items VALUES (3004, 2001, 1, 28000);

INSERT INTO reviews VALUES (4001, 2001, 1002, 5, DATE '2026-08-20');

COMMIT;

02_cache_demo.sql

SET SERVEROUTPUT ON

PROMPT ===== 1차 실행 =====
DECLARE
  v_phy_before  NUMBER;
  v_phy_after   NUMBER;
  v_cons_before NUMBER;
  v_cons_after  NUMBER;
  v_cnt         NUMBER;
BEGIN
  SELECT value INTO v_phy_before
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'physical reads';

  SELECT value INTO v_cons_before
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'consistent gets';

  SELECT COUNT(*) INTO v_cnt FROM orders WHERE member_id = 1004;

  SELECT value INTO v_phy_after
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'physical reads';

  SELECT value INTO v_cons_after
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'consistent gets';

  DBMS_OUTPUT.PUT_LINE('조회 건수: ' || v_cnt);
  DBMS_OUTPUT.PUT_LINE('physical reads 증가량: ' || (v_phy_after - v_phy_before));
  DBMS_OUTPUT.PUT_LINE('consistent gets 증가량: ' || (v_cons_after - v_cons_before));
END;
/

PROMPT ===== 2차 실행(같은 조건) =====
DECLARE
  v_phy_before  NUMBER;
  v_phy_after   NUMBER;
  v_cons_before NUMBER;
  v_cons_after  NUMBER;
  v_cnt         NUMBER;
BEGIN
  SELECT value INTO v_phy_before
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'physical reads';

  SELECT value INTO v_cons_before
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'consistent gets';

  SELECT COUNT(*) INTO v_cnt FROM orders WHERE member_id = 1004;

  SELECT value INTO v_phy_after
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'physical reads';

  SELECT value INTO v_cons_after
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'consistent gets';

  DBMS_OUTPUT.PUT_LINE('조회 건수: ' || v_cnt);
  DBMS_OUTPUT.PUT_LINE('physical reads 증가량: ' || (v_phy_after - v_phy_before));
  DBMS_OUTPUT.PUT_LINE('consistent gets 증가량: ' || (v_cons_after - v_cons_before));
END;
/

PROMPT ===== 커밋 시 redo size 변화 =====
DECLARE
  v_redo_before NUMBER;
  v_redo_after  NUMBER;
BEGIN
  SELECT value INTO v_redo_before
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'redo size';

  UPDATE orders SET status = 'DONE' WHERE order_id = 3004;
  COMMIT;

  SELECT value INTO v_redo_after
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'redo size';

  DBMS_OUTPUT.PUT_LINE('커밋 후 redo size 증가량(byte): ' || (v_redo_after - v_redo_before));
END;
/

줄별 해설

  • CONSTRAINT fk_orders_member FOREIGN KEY (member_id) REFERENCES members(member_id)는 참조 무결성 제약이다. MEMBERS에 없는 회원 번호로는 ORDERS를 만들 수 없다.
  • CREATE INDEX ix_orders_member ON orders(member_id)는 회원별 주문 조회가 잦다는 것을 가정해 만든 인덱스다. 결합 인덱스의 컬럼 순서 같은 설계 기준은 인덱스를 본격적으로 다루는 장에서 정리한다.
  • v$mystat은 통계값(value)만 담고 있고 통계 이름은 v$statname에 있어서, 원하는 통계를 찾으려면 statistic#으로 두 뷰를 조인해야 한다.
  • v_phy_before·v_phy_after처럼 실행 전후 값을 따로 받는 이유는 통계가 세션이 접속한 뒤부터 누적된 값이기 때문이다. 이번 실행에서 늘어난 양만 보려면 전후 값의 차이를 구해야 한다.
  • 1차 실행의 SELECT COUNT(*) FROM orders WHERE member_id = 1004는 인덱스를 거쳐 데이터 파일에서 블록을 읽어 오므로 physical reads가 늘어난다.
  • 2차 실행은 같은 블록을 다시 찾지만 이미 버퍼 캐시에 있으므로 physical reads는 늘지 않고 consistent gets(논리 읽기)만 발생한다.
  • UPDATE orders SET status = 'DONE' ... COMMIT;에서 UPDATE 자체는 버퍼 캐시의 블록만 바꾸고, COMMIT 순간 로그 버퍼 내용이 LGWR에 의해 REDO 로그 파일로 옮겨지면서 redo size 통계가 증가한다.

실행 결과

아래는 한 환경에서 실행했을 때의 예시 값이다. 물리 읽기·논리 읽기 수치는 버퍼 캐시 상태와 서버 환경에 따라 달라질 수 있지만, "1차는 physical reads가 있고 2차는 없다"는 방향은 항상 같다.

SQL> @01_schema.sql
SQL> @02_cache_demo.sql
===== 1차 실행 =====
조회 건수: 3
physical reads 증가량: 6
consistent gets 증가량: 7
===== 2차 실행(같은 조건) =====
조회 건수: 3
physical reads 증가량: 0
consistent gets 증가량: 7
===== 커밋 시 redo size 변화 =====
커밋 후 redo size 증가량(byte): 164

실무에서 자주 틀리는 것

커밋하면 데이터 파일도 바로 바뀐다는 착각

COMMIT이 보장하는 것은 REDO 로그 파일 기록이지 데이터 파일 반영이 아니다. 데이터 파일 갱신 여부로 커밋 성공을 판단하면 매번 잘못된 결론에 도달한다.

-- 틀린 방식: 커밋 후 데이터 파일이 바로 바뀌었을 거라 가정하고 끝낸다
UPDATE orders SET status = 'DONE' WHERE order_id = 3004;
COMMIT;
-- 이후 데이터 파일 자체만 들여다보며 "왜 안 바뀌었지" 라고 의심한다
-- 고친 방식: 커밋이 보장하는 redo 기록을 직접 확인한다
DECLARE
  v_before NUMBER;
  v_after  NUMBER;
BEGIN
  SELECT value INTO v_before
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'redo size';

  UPDATE orders SET status = 'DONE' WHERE order_id = 3004;
  COMMIT;

  SELECT value INTO v_after
    FROM v$mystat s JOIN v$statname n ON n.statistic# = s.statistic#
   WHERE n.name = 'redo size';

  DBMS_OUTPUT.PUT_LINE('redo 증가량: ' || (v_after - v_before));
END;
/

인스턴스 장애가 곧 데이터 유실이라는 착각

데이터 파일이 손상되지 않았다면 인스턴스를 다시 띄우는 것만으로 충분하다. 시작 과정에서 REDO 로그를 읽어 커밋된 변경을 자동으로 다시 적용하기 때문이다.

-- 틀린 대응: 비정상 종료를 데이터 유실로 단정하고 곧바로 전체 복원부터 시도한다
RMAN> RESTORE DATABASE;
RMAN> RECOVER DATABASE;
-- 고친 대응: 데이터 파일 손상이 확인되지 않았다면 우선 인스턴스만 기동한다
SQL> STARTUP
-- SMON이 온라인 REDO 로그를 읽어 커밋된 변경을 자동으로 복구한다

버퍼 캐시와 OS 파일 캐시를 같은 것으로 다루기

버퍼 캐시는 인스턴스가 소유한 SGA 내부 메모리이고, OS 파일 캐시는 운영체제가 관리하는 별개의 캐시다. OS 캐시를 비운다고 버퍼 캐시가 초기화되지 않는다.

-- 틀린 진단: OS 캐시를 지우면 DB 성능이 원래대로 돌아온다고 믿는다
$ sync && echo 3 > /proc/sys/vm/drop_caches
-- 고친 진단: 버퍼 캐시 사용 현황은 DB 쪽 뷰로 확인한다
SELECT name, value
  FROM v$sysstat
 WHERE name IN ('physical reads', 'consistent gets');

커밋 지연을 진단 없이 서버 증설로 해결하려는 습관

커밋이 느리다는 보고를 받으면 CPU나 메모리부터 늘리기 쉽지만, 원인은 대부분 로그 파일이 놓인 디스크의 쓰기 지연이다. 먼저 LGWR 관련 대기 이벤트를 확인해야 한다.

-- 틀린 대응: 원인 확인 없이 서버 스펙부터 늘린다
-- 고친 대응: 커밋 지연의 원인을 대기 이벤트에서 먼저 찾는다
SELECT event, total_waits, time_waited
  FROM v$system_event
 WHERE event = 'log file sync';

한눈에 보기

SGA 구성요소와 디스크 반영 시점
구성요소위치커밋 시 반영 여부역할
버퍼 캐시메모리(SGA)아니오(나중에 DBWn이 반영)최근 읽거나 바꾼 데이터 블록 보관
로그 버퍼메모리(SGA)예(LGWR이 즉시 반영)변경 내용을 REDO 형태로 임시 보관
공유 풀메모리(SGA)해당 없음SQL·실행계획·딕셔너리 캐시
데이터 파일디스크아니오(비동기)테이블·인덱스 데이터 저장
REDO 로그 파일디스크예커밋을 보장하는 변경 이력 저장

연습 문제

  1. 같은 SELECT 문을 두 번 연속 실행했을 때 두 번째 실행이 훨씬 빠른 이유를 버퍼 캐시 관점에서 설명하라.
  2. 다음 설명 중 옳은 것을 모두 고르라. (가) COMMIT은 데이터 파일에 변경 내용을 즉시 반영한다. (나) COMMIT은 로그 버퍼 내용을 REDO 로그 파일에 즉시 반영한다. (다) 더티 블록은 COMMIT 이후에도 한동안 버퍼 캐시에만 남아 있을 수 있다.
  3. REVIEWS 테이블에서 특정 도서의 리뷰를 조회하는 쿼리를 처음 실행할 때와 바로 다시 실행할 때, physical reads와 consistent gets는 각각 어떻게 달라질지 서술하라.
  4. 인스턴스가 비정상 종료된 뒤 재시작했을 때 커밋된 데이터가 사라지지 않는 이유를 REDO 로그와 연결해 설명하라.

정답과 해설

  1. 첫 실행에서 필요한 블록이 버퍼 캐시에 없어 데이터 파일에서 물리 읽기로 가져오고, 그 블록을 캐시에 적재한다. 두 번째 실행은 같은 블록을 캐시에서 바로 찾으므로 물리 읽기 없이 논리 읽기만으로 끝나 훨씬 빠르다.
  2. (나)와 (다)가 옳다. COMMIT이 보장하는 것은 REDO 로그 파일 기록이며, 데이터 파일 반영은 DBWn이 비동기로 나중에 처리한다. (가)는 커밋과 데이터 파일 반영 시점을 혼동한 설명이라 틀렸다.
  3. 처음 조회는 IX_REVIEWS_BOOK 인덱스와 REVIEWS 테이블 블록을 읽기 위해 데이터 파일까지 접근하므로 physical reads가 늘어난다. 바로 다시 조회하면 같은 블록이 이미 버퍼 캐시에 있으므로 physical reads는 거의 늘지 않고, 블록을 훑는 과정에서 발생하는 consistent gets는 두 번 모두 비슷한 수준으로 나타난다.
  4. COMMIT 시점에 LGWR이 로그 버퍼 내용을 REDO 로그 파일에 이미 기록해 두었기 때문이다. 인스턴스가 다시 뜰 때 REDO 로그를 순서대로 읽어 커밋된 변경을 데이터 파일에 다시 적용하는 크래시 복구가 자동으로 실행되므로, 데이터 파일 자체가 아직 반영 전이었더라도 결과적으로 커밋된 내용은 복구된다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.