Devin.KR

블록 단위 I/O - 논리 읽기와 물리 읽기

개발자KR 조회 2

이 장에서 배우는 것

실행계획에 찍히는 Rows 와 Cost 는 결과일 뿐이고, 그 뒤에서 실제로 벌어지는 일은 블록(Block) 단위의 입출력이다. 오라클은 행 하나를 읽기 위해서도 그 행이 들어 있는 블록 전체를 읽어 들이며, 이 블록을 버퍼 캐시(Buffer Cache)에서 찾았는지 디스크에서 가져왔는지에 따라 같은 쿼리도 체감 속도가 크게 달라진다. 이 장은 그 I/O 가 어떤 단위로, 어떤 방식으로 일어나는지, 그리고 튜닝할 때 어떤 숫자를 신뢰해야 하는지를 다룬다.

  • 블록·익스텐트·세그먼트의 저장 계층 구조를 설명할 수 있다
  • 싱글 블록 I/O 와 멀티 블록 I/O 가 각각 언제 일어나는지 구분할 수 있다
  • 논리 읽기(consistent gets)가 물리 읽기보다 신뢰할 수 있는 튜닝 지표인 이유를 설명할 수 있다
  • 버퍼 캐시 적중률이 높아도 성능이 나쁠 수 있는 상황을 식별할 수 있다

문제 상황

온라인 서점 운영팀에서 야간 배치로 돌리는 취소 주문 집계 쿼리가 느리다는 보고가 올라왔다. 담당 DBA 는 V$SYSSTAT 로 계산한 인스턴스 버퍼 캐시 적중률을 확인했는데 99% 로 나왔다. "캐시 적중률이 99% 면 거의 다 메모리에서 처리된다는 뜻이니 문제 될 게 없다"고 결론짓고 보고를 종료하려 했다. 그런데 같은 시간대에 다른 화면 조회(member_id 로 주문 조회)는 체감상 즉시 끝나는 반면, 취소 주문 집계 쿼리는 매번 몇 초씩 걸렸다. 두 쿼리 모두 캐시 적중률은 100% 에 가까웠는데 왜 이렇게 차이가 나는지, 히트율이라는 숫자 하나만으로는 설명이 되지 않았다. 이 장은 이 질문에 답하기 위해 필요한 개념을 다룬다.

블록·익스텐트·세그먼트 - 저장의 최소 단위

오라클은 데이터를 행 단위가 아니라 블록(Block) 단위로 읽고 쓴다. 블록 크기는 DB_BLOCK_SIZE 파라미터로 정해지며 흔히 8K 를 쓴다. 이 크기는 데이터베이스(또는 테이블스페이스) 생성 시점에 결정되고 이후에는 바꾸기 어렵다. 연속된 블록 여러 개를 묶은 단위가 익스텐트(Extent)이고, 테이블이나 인덱스 같은 오브젝트 하나가 차지하는 익스텐트의 집합이 세그먼트(Segment)다. 오브젝트가 커지면 오라클은 새 익스텐트를 추가로 할당해 세그먼트를 키운다.

주문 테이블처럼 1천만 건 규모의 세그먼트는 처음에는 작은 익스텐트 몇 개로 시작하지만 데이터가 쌓이면서 익스텐트 수가 늘어난다. 이 구조를 알아 둬야 하는 이유는, 뒤에서 볼 논리 읽기·물리 읽기가 결국 "블록을 얼마나 많이, 어떤 순서로 건드리는가"의 문제이기 때문이다.

세그먼트는 여러 익스텐트로, 익스텐트는 여러 블록으로 구성된다

싱글 블록 I/O 와 멀티 블록 I/O

오라클이 블록을 읽어 오는 방식은 크게 두 가지다. 필요한 블록을 하나씩 개별적으로 요청하는 싱글 블록 I/O(Single Block I/O)와, 연속된 블록 여러 개를 한 번의 호출로 요청하는 멀티 블록 I/O(Multi-block I/O)다. 어느 쪽이 쓰이는지는 실행계획의 접근 경로가 결정한다.

인덱스 접근과 싱글 블록 I/O

인덱스를 거쳐 테이블 블록을 찾아가는 경우(INDEX RANGE SCAN 뒤의 TABLE ACCESS BY INDEX ROWID 등)는 필요한 블록의 위치를 그때그때 알아내어 하나씩 요청한다. 이때 발생하는 대기 이벤트가 db file sequential read 다. 인덱스로 찾아가는 행들이 테이블 안에서 서로 멀리 떨어져 있으면 블록 요청 횟수가 그만큼 늘어난다.

풀 스캔과 멀티 블록 I/O

세그먼트를 처음부터 끝까지 순서대로 읽는 TABLE ACCESS FULL, INDEX FAST FULL SCAN 은 DB_FILE_MULTIBLOCK_READ_COUNT 파라미터가 허용하는 만큼 여러 블록을 한 번의 호출로 읽는다. 이때 나타나는 대기 이벤트가 db file scattered read 다. 다만 대상 세그먼트가 충분히 크면 오라클은 버퍼 캐시를 거치지 않고 곧바로 디스크에서 읽어 오는 direct path read 방식을 택할 수 있다. 이 경우는 버퍼 캐시 적중률 계산 자체에서 벗어나므로, 뒤에서 볼 히트율의 함정을 더 심하게 만드는 요인이 된다.

인덱스 스캔은 블록을 하나씩, 풀 스캔은 여러 블록을 한 번에 읽는다
싱글/멀티 블록 I/O 관련 오라클과 MySQL 용어 비교
구분Oracle 19cMySQL 8 (InnoDB)비고
기본 저장 단위블록(기본 8K, DB_BLOCK_SIZE)페이지(기본 16K, innodb_page_size)이름만 다르고 개념은 같다
싱글 블록 I/O 흔적db file sequential read 대기 이벤트핸들러 카운터(Handler_read_rnd_next 등)모니터링 체계가 다르다
멀티 블록 읽기 제어DB_FILE_MULTIBLOCK_READ_COUNTinnodb_read_ahead_threshold(선형 read-ahead)오라클은 값 직접 조정, MySQL은 임계값 기반 자동 판단
캐시 영역버퍼 캐시(SGA 내부)버퍼 풀(innodb_buffer_pool_size)이름만 다르고 역할은 같다

논리 읽기와 버퍼 캐시 적중률의 함정

논리 읽기(consistent gets)가 신뢰할 수 있는 지표인 이유

블록을 요청할 때 오라클은 항상 버퍼 캐시부터 확인한다. 캐시에 있으면 그대로 쓰고, 없으면 디스크에서 읽어 캐시에 올린 뒤 쓴다. 어느 경로든 "버퍼 캐시에서 블록을 사용했다"는 사실은 똑같이 남고, 이 횟수를 센 것이 논리 읽기(Logical Read, 읽기 일관성을 보장하는 조회에서는 consistent gets 로 집계된다)다. 반면 물리 읽기(Physical Read)는 캐시에 없어서 디스크까지 다녀온 경우에만 추가로 발생한다. 즉 논리 읽기는 캐시 상태와 무관하게 그 쿼리가 처리해야 하는 작업량을 그대로 보여주고, 물리 읽기는 그날 캐시가 얼마나 채워져 있었는지에 따라 실행할 때마다 달라진다. 그래서 실행계획을 두 번 이상 반복 측정해 비교할 때는 Reads 가 아니라 Buffers(논리 읽기)를 기준으로 삼아야 한다.

논리 읽기는 캐시 적중 여부와 관계없이 항상 발생하지만 물리 읽기는 캐시 미스일 때만 추가된다

히트율이 높아도 느릴 수 있는 이유

버퍼 캐시 적중률(Hit Ratio)은 (논리 읽기 - 물리 읽기) / 논리 읽기 로 계산한다. 이 값은 물리 읽기 대비 비율만 보여줄 뿐 논리 읽기 자체의 절대량은 알려주지 않는다. 뒤의 완성 코드에서 확인하겠지만, 회원별 주문 조회처럼 인덱스로 10건만 찾는 쿼리도, 취소 주문을 전부 세는 풀 스캔 쿼리도 캐시가 한 번 워밍되고 나면 똑같이 히트율 100% 를 기록한다. 하지만 전자는 블록 13개, 후자는 블록 2,634개를 논리 읽기한다. 히트율만 보면 둘 다 "문제없음"으로 보이지만 실제 작업량은 200배 넘게 차이가 난다. 문제 상황에서 DBA 가 인스턴스 히트율 99% 만 보고 넘어가려 했던 것이 바로 이 함정이다.

완성 코드

-- block_io_demo.sql
-- 목적: 논리 읽기(Buffers)와 물리 읽기(Reads)를 DBMS_XPLAN 으로 직접 확인한다.

SET SERVEROUTPUT ON
SET LINESIZE 160
SET PAGESIZE 100

BEGIN
   EXECUTE IMMEDIATE 'DROP TABLE orders_demo PURGE';
EXCEPTION
   WHEN OTHERS THEN
      IF SQLCODE != -942 THEN
         RAISE;
      END IF;
END;
/

CREATE TABLE orders_demo (
   order_id     NUMBER        NOT NULL,
   member_id    NUMBER        NOT NULL,
   order_date   DATE          NOT NULL,
   status       VARCHAR2(10)  NOT NULL,
   total_amount NUMBER(10)    NOT NULL,
   CONSTRAINT pk_orders_demo PRIMARY KEY (order_id)
);

INSERT /*+ APPEND */ INTO orders_demo
SELECT LEVEL,
       MOD(LEVEL, 50000) + 1,
       DATE '2024-01-01' + MOD(LEVEL, 700),
       CASE MOD(LEVEL, 5) WHEN 0 THEN '취소' ELSE '완료' END,
       MOD(LEVEL, 200) * 1000 + 9000
FROM   dual
CONNECT BY LEVEL <= 500000;
COMMIT;

CREATE INDEX ix_orders_demo_member ON orders_demo (member_id);

EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'ORDERS_DEMO', CASCADE => TRUE);

-- 물리 읽기를 재현하려고 버퍼 캐시를 비운다(운영 DB에서는 실행하지 않는다).
ALTER SYSTEM FLUSH BUFFER_CACHE;

VARIABLE b1 NUMBER
EXEC :b1 := 100;

-- 1) 인덱스로 회원 한 명의 주문을 찾는 쿼리 (콜드 캐시)
SELECT /*+ gather_plan_statistics */ order_id, order_date, status, total_amount
FROM   orders_demo
WHERE  member_id = :b1;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

-- 2) 같은 쿼리를 한 번 더 실행 (버퍼 캐시가 이미 채워진 상태)
SELECT /*+ gather_plan_statistics */ order_id, order_date, status, total_amount
FROM   orders_demo
WHERE  member_id = :b1;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

-- 3) 취소 주문을 세는 풀 스캔 쿼리 (콜드 캐시 상태에서 이어서 실행)
SELECT /*+ gather_plan_statistics full(orders_demo) */
       COUNT(*)
FROM   orders_demo
WHERE  status = '취소';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

-- 4) 같은 풀 스캔 쿼리를 한 번 더 실행
SELECT /*+ gather_plan_statistics full(orders_demo) */
       COUNT(*)
FROM   orders_demo
WHERE  status = '취소';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

줄별 해설

첫 번째 PL/SQL 블록은 이전 실행에서 만든 orders_demo 테이블이 남아 있으면 지우고, 없으면(ORA-00942) 조용히 넘어간다. CREATE TABLE 은 회원·도서 주문 테이블을 단순화한 데모용 주문 테이블로, order_id 를 기본키로 둔다.

INSERT ... CONNECT BY LEVEL 구문은 dual 에서 행을 50만 개 만들어 곧바로 orders_demo 에 넣는다. member_id 는 MOD(LEVEL, 50000)+1 로 계산해 회원 5만 명에게 평균 10건씩 주문이 돌아가게 하고, status 는 5건 중 1건꼴로 '취소'가 나오게 했다.

CREATE INDEX 로 member_id 단일 컬럼 인덱스를 만들고, DBMS_STATS.GATHER_TABLE_STATS 로 통계를 수집해 옵티마이저가 이 인덱스를 쓸지 판단할 근거를 마련한다.

ALTER SYSTEM FLUSH BUFFER_CACHE 는 버퍼 캐시를 강제로 비워 콜드 상태를 재현하는 명령이다. 인스턴스 전체 캐시에 영향을 주므로 반드시 개인 테스트 환경에서만 실행한다.

1)~4)번 쿼리는 모두 gather_plan_statistics 힌트를 붙여 실행 후 DBMS_XPLAN.DISPLAY_CURSOR(..., 'ALLSTATS LAST') 로 방금 실행에서 실제로 발생한 Buffers(논리 읽기)와 Reads(물리 읽기)를 조회한다. 1)과 2)는 같은 인덱스 조회 쿼리를 콜드·웜 상태에서 각각 실행하고, 3)과 4)는 같은 풀 스캔 쿼리를 콜드·웜 상태에서 각각 실행해 두 접근 방식의 논리 읽기 규모를 나란히 비교한다.

실행 결과

$ sqlplus demo/demo@orclpdb1 @block_io_demo.sql

SQL_ID  9fkbv82h1x3q1, child number 0
-------------------------------------
select /*+ gather_plan_statistics */ order_id, order_date, status,
total_amount from orders_demo where member_id = :b1

------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name                   | Starts | A-Rows | Buffers | Reads |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                        |      1 |     10 |      13 |    13 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED  | ORDERS_DEMO            |      1 |     10 |      13 |    13 |
|*  2 |   INDEX RANGE SCAN                    | IX_ORDERS_DEMO_MEMBER  |      1 |     10 |       3 |     3 |
------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("MEMBER_ID"=:B1)

-- 두 번째 실행(웜 캐시) --------------------------------------------------

SQL_ID  9fkbv82h1x3q1, child number 0
------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name                   | Starts | A-Rows | Buffers | Reads |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                        |      1 |     10 |      13 |     0 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED  | ORDERS_DEMO            |      1 |     10 |      13 |     0 |
|*  2 |   INDEX RANGE SCAN                    | IX_ORDERS_DEMO_MEMBER  |      1 |     10 |       3 |     0 |
------------------------------------------------------------------------------------------------------------

-- 세 번째 실행: 풀 스캔, 콜드 캐시 ----------------------------------------

SQL_ID  2m9xk7c4bq8sw, child number 0
select /*+ gather_plan_statistics full(orders_demo) */ count(*)
from orders_demo where status = '취소'

------------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name          | Starts | A-Rows | Buffers | Reads  |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |               |      1 |      1 |    2634 |   2634 |
|   1 |  SORT AGGREGATE     |               |      1 |      1 |    2634 |   2634 |
|*  2 |   TABLE ACCESS FULL | ORDERS_DEMO   |      1 | 100000 |    2634 |   2634 |
------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter("STATUS"='취소')

-- 네 번째 실행: 풀 스캔, 웜 캐시 -----------------------------------------

SQL_ID  2m9xk7c4bq8sw, child number 0
------------------------------------------------------------------------------------------------------------
| Id  | Operation           | Name          | Starts | A-Rows | Buffers | Reads  |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |               |      1 |      1 |    2634 |      0 |
|   1 |  SORT AGGREGATE     |               |      1 |      1 |    2634 |      0 |
|*  2 |   TABLE ACCESS FULL | ORDERS_DEMO   |      1 | 100000 |    2634 |      0 |
------------------------------------------------------------------------------------------------------------

두 번째와 네 번째 실행 모두 Reads 는 0이다. 즉 히트율만 보면 둘 다 100% 다. 그러나 Buffers 는 13과 2,634 로, 인덱스 조회가 처리한 블록 수의 200배가 넘는 블록을 풀 스캔이 논리적으로 읽었다. 문제 상황에서 두 쿼리의 체감 속도가 달랐던 이유가 바로 이 차이다.

실무에서 자주 틀리는 것

히트율만 보고 튜닝을 끝냈다고 판단

-- 틀린 코드: 인스턴스 전체 평균만 보고 종료
SELECT ROUND((1 - (phy.value / (cur.value + con.value))) * 100, 2) AS hit_ratio
FROM   v$sysstat cur, v$sysstat con, v$sysstat phy
WHERE  cur.name = 'db block gets'
AND    con.name = 'consistent gets'
AND    phy.name = 'physical reads';

인스턴스 평균은 수많은 세션의 값을 섞은 숫자라 특정 SQL 한 건의 논리 읽기 폭증을 가려 버린다.

-- 고친 코드: 문제가 의심되는 SQL 을 직접 지목해 논리 읽기 절대량을 확인
SELECT sql_id, executions, buffer_gets,
       ROUND(buffer_gets / NULLIF(executions, 0)) AS gets_per_exec
FROM   v$sql
WHERE  parsing_schema_name = USER
ORDER  BY buffer_gets DESC
FETCH FIRST 10 ROWS ONLY;

ALTER SYSTEM FLUSH BUFFER_CACHE 를 운영 환경에서 실행

-- 틀린 코드: 운영 DB 에서 실행 금지
ALTER SYSTEM FLUSH BUFFER_CACHE;

전체 인스턴스의 버퍼 캐시를 한 번에 비우므로 이 순간 모든 세션이 콜드 캐시 상태가 되어 서비스 전체가 순간적으로 느려진다.

-- 고친 코드: 캐시를 비우지 않고 실행 전후 누적치 차이로 비교
SELECT sql_id, executions, buffer_gets, disk_reads
FROM   v$sql
WHERE  sql_id = '&sql_id';
-- 쿼리를 한 번 더 실행한 뒤 같은 조회를 반복해 증가분을 비교한다.

SET AUTOTRACE TRACEONLY EXPLAIN 으로 실제 I/O 를 확인했다고 착각

-- 틀린 코드: EXPLAIN 은 쿼리를 실행하지 않고 예상 계획만 보여준다
SET AUTOTRACE TRACEONLY EXPLAIN
SELECT * FROM orders_demo WHERE member_id = 100;

이 옵션은 실제로 쿼리를 실행하지 않으므로 Buffers·Reads 값이 아예 출력되지 않는다.

-- 고친 코드: 실제 실행 통계를 함께 확인
SET AUTOTRACE TRACEONLY STATISTICS
SELECT * FROM orders_demo WHERE member_id = 100;
-- 또는
SELECT /*+ gather_plan_statistics */ * FROM orders_demo WHERE member_id = 100;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

작은 개발 DB 에서 빠르다고 검증하고 그대로 배포

-- 틀린 코드: 개발 DB(주문 1만 건)에서만 확인
SELECT COUNT(*) FROM orders WHERE status = '취소';
-- Buffers 26, Reads 0 -> "충분히 빠르다"고 판단하고 그대로 배포

블록 수가 데이터량에 비례하는 값이라, 운영 규모(1천만 건)에서는 Buffers 가 함께 커져 버퍼 캐시에 다 못 올라가면 물리 읽기가 급증한다.

-- 고친 코드: 배포 전 운영과 비슷한 규모로 확인
SELECT /*+ gather_plan_statistics */ COUNT(*)
FROM   orders
WHERE  status = '취소';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));
-- Buffers 가 전체 블록 수에 비례해 커지는지 확인하고
-- 조건절 선택도가 낮다면 인덱스 스캔으로 유도한다.

한눈에 보기

블록 단위 I/O 핵심 개념 정리
개념의미확인 방법주의점
블록I/O 를 수행하는 최소 단위DB_BLOCK_SIZE 파라미터테이블스페이스 생성 후 변경이 사실상 불가능
익스텐트연속된 블록의 묶음DBA_EXTENTS오브젝트가 커질 때마다 새로 할당된다
세그먼트익스텐트의 집합(테이블·인덱스 등)DBA_SEGMENTS파티션을 쓰면 파티션마다 별도 세그먼트가 된다
논리 읽기버퍼 캐시에서 블록을 읽은 횟수Buffers(DBMS_XPLAN), V$SQL.BUFFER_GETS캐시 적중 여부와 무관하게 항상 발생, 가장 신뢰할 수 있는 작업량 지표
물리 읽기디스크에서 블록을 읽은 횟수Reads(DBMS_XPLAN), V$SQL.DISK_READS캐시 상태에 따라 실행마다 값이 크게 달라진다
버퍼 캐시 적중률(논리 읽기-물리 읽기)/논리 읽기V$SYSSTAT 계산값절대 작업량을 숨기므로 단독 판단 지표로는 부적절

연습 문제

  1. 같은 쿼리를 콜드 캐시와 웜 캐시 상태에서 각각 실행해 Buffers 는 두 번 모두 13, Reads 는 각각 13과 0 이 나왔다. 이 결과만 보고 "두 번째 실행부터는 성능에 문제없다"고 결론지어도 되는지, 논리 읽기 값 13이 의미하는 바와 함께 설명하라.
  2. 버퍼 캐시 적중률이 99% 인 인스턴스에서 특정 배치 쿼리의 체감 성능이 나쁘다는 보고를 받았다. 본문 표의 결과를 근거로, 히트율만으로 이 문제를 판단할 수 없는 이유를 설명하라.
  3. 인덱스 유니크 스캔과 풀 테이블 스캔 중 어느 쪽이 싱글 블록 I/O 위주이고 어느 쪽이 멀티 블록 I/O 위주인지 쓰고, 그 이유를 설명하라.
  4. 캐시가 이미 충분히 워밍된 상태인데도 같은 쿼리를 반복 실행할 때마다 Buffers 는 그대로인데 Reads 값이 0과 0이 아닌 값 사이를 오간다면 무엇을 의심해야 하는지 서술하라.

정답과 해설

1번. Reads=0 은 그 실행에서 디스크 I/O 가 없었다는 뜻일 뿐이다. 오라클이 실제로 처리한 작업량은 Buffers=13, 즉 블록 13개를 논리적으로 읽어야 했다는 사실이며 이는 캐시 상태와 무관하게 그대로 남는다. 데이터가 늘어 매칭되는 행이 많아지면 이 값 자체가 커지고, 그러면 캐시에 다 올라가 있어도(Reads=0 이어도) CPU·래치 비용이 함께 늘어 느려질 수 있다. 따라서 캐시 히트 여부가 아니라 Buffers 의 절대량을 기준으로 판단해야 한다.

2번. 본문 표에서 인덱스 스캔과 풀 스캔은 둘 다 웜 캐시 상태에서 히트율 100% 를 기록하지만 Buffers 는 13과 2,634 로 200배 넘게 차이가 난다. 히트율은 물리 읽기 대비 논리 읽기의 비율만 보여줄 뿐 논리 읽기의 절대 규모는 알려주지 않는다. 인스턴스 전체 히트율 99% 는 대부분의 세션이 캐시에서 처리된다는 뜻일 뿐, 특정 배치 쿼리가 한 번 실행에 수천~수백만 블록을 논리 읽기하며 CPU 를 소모하고 있어도 전체 평균에 묻혀 드러나지 않는다. 문제의 SQL 을 V$SQL.BUFFER_GETS 나 DBMS_XPLAN 의 Buffers 값으로 따로 확인해야 한다.

3번. 인덱스 유니크·레인지 스캔은 리프 블록에서 알아낸 위치로 테이블 블록을 하나씩 랜덤하게 찾아가므로 싱글 블록 I/O(db file sequential read) 위주다. 풀 테이블 스캔은 세그먼트를 처음부터 끝까지 순서대로 읽으므로 DB_FILE_MULTIBLOCK_READ_COUNT 만큼 여러 블록을 한 번의 호출로 읽는 멀티 블록 I/O(db file scattered read 또는 direct path read) 위주다.

4번. Buffers 는 그 쿼리가 처리해야 하는 블록 수이므로 데이터 분포가 바뀌지 않는 한 일정해야 정상이다. 이 값이 일정한데도 Reads 가 들쭉날쭉하다면, 다른 세션들이 동시에 같은 버퍼 캐시를 쓰면서 LRU 알고리즘에 의해 이 쿼리가 쓰던 블록이 캐시에서 밀려났다가 다시 채워지기를 반복하고 있다고 의심해야 한다. 즉 버퍼 캐시 크기 대비 여러 세션의 작업 집합(working set) 합이 너무 커서 캐시 경합이 발생하는 상황이다.

댓글 0

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

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