테마
15b화 · 같은 답을 두 번 계산하지 마라
🗓️ 6월 17일 (월) · 15화에서 일주일 뒤 · 무대
ora-hanorder(한빛유통) · 3막의 막간오늘 배울 것: 캐시는 네 겹이고, 결과 캐시는 '같은 질문'인지를 인자와 테이블만 보고 판단한다. 그리고 오늘은 선배가 틀린다. 이번 조각은 6조각 중 하나가 아니라 함수 하나다. 정산일 함수SDT_rev00 → rev01(15화의ADJ처럼 새로 생긴 줄).
파일 세 개
| 파일 | 무엇을 하나 | 시간 | |
|---|---|---|---|
| 읽는다 | 1_사건.md | 금요일에 붙인 한 단어 때문에 정산일이 6/14에 얼어붙은 월요일. 함수를 되돌리고, 캐시가 무엇에 기대어 답을 적었는지 보고, 7층 복도의 화이트보드 앞에 서고, 시계를 건드리지 않고 '어제의 답을 오늘도' 돌려주는 장면을 재현하고, 그 한 단어로 얻은 게 없었다는 걸 알고, 네 겹의 캐시를 나란히 재고, 정산일 함수를 시험할 수 있게 고치는 하루 | 35분 |
| 한다 | 3_실습과_기록.md | 얼어붙은 정산일 재현 · 캐싱 4계층 · 무효화 · FORCE · 경합 · (선택) ENFORCED · T1 11호 · 코딩 가이드 조문 11 | 60~90분 |
| 찾는다 | 2_파고들기.md | 4계층 표 · Server / PL/SQL Function Result Cache · 칸(1,984) · 무효화 실측 · 숨은 입력 넷 · 재현 방법 · TRUSTED / ENFORCED · DETERMINISTIC의 범위 · MANUAL / FORCE · 래치 · 판단 순서 | 필요할 때 |
1을 읽고 3을 따라 한다. 2는 막힐 때 펼친다. 시간이 없는 주에는 1_사건을 중단 지점 1까지 읽고 3_실습은 STEP 0, 1, 2, 3, 8만 한다(약 40분).
지난 화에서 들고 오는 것
| 15화 | "등급표를 패키지 변수에": UGA · 재컴파일 → ORA-04068 · "화면에 줄 거면 결과 캐시라는 게 따로 있어. 그건 다음 주 15b에서" · 보정률 함수 FN_ADJ_RATE(정산 등록 화면의 예상 보정액도 이 함수) · 스칼라 서브쿼리 캐시가 고객 2,000명을 다 못 담음(57,679) |
| 07화 | 탕비실 픽업 선반(버퍼 캐시 · 블록) · 책상 위 메모판(스칼라 서브쿼리 캐시 · NDV 2,000 → Starts 752,995) · "실행을 넘어 결과를 재사용하는 Result Cache와 그 대가는 15b화" |
| 08화 | "같은 3행을 실행마다 다시 계산하지 않기: 결과 재사용과 그 대가 → 15b화" · 「정합성 회귀 체크리스트」 A · E · F · G |
| 02화 | "이 데이터에서 같다를 맞다로 읽지 마라"(T_ORD_BIG은 전부 자정) |
그리고 월요일 아침, 메시지가 하나 와 있다.
"오늘 아침에 등록한 정산이 전부 정산일 6/14(금)로 찍혀 있어요. 금요일 저녁 6시 넘어서 등록한 것도 6/14고요." (조민우, 09:11)
오늘의 전개 — 선배가 틀리고, 내가 한 번 틀린다
| 믿은 것 | 측정 (연습 DB 복사본 · 컨테이너 시계는 그대로) | |
|---|---|---|
| 09:15 | (선배, 금요일) "인자가 없고 작성자가 DETERMINISTIC이라고 했으니 언제 불러도 같은 답이다. RESULT_CACHE를 붙인다" | 6/14 17:30 반영 → 6/17 09:40 되돌림. 그동안 정산일 6/14 고정 |
| 09:45 | (나) "정산이 계속 등록됐는데 캐시가 안 지워졌다. 무효화가 고장 났다" | 정산일 답의 의존 객체 = 함수 자신 하나. 정산 등록 → Published · 고객 마스터 커밋 → 보정률만 Invalid. 시계는 객체가 아니다 |
| 10:00 | (비유) 7층 복도의 화이트보드 | "오늘 등록하면 정산일은? → 6/14 (금)" · 지우는 규칙은 "규정집이 바뀌면" |
| 10:20 | 하루를 기다릴 수 없다 | '초' 판이 첫 값에 멈춤 · 목록 100,001 vs 100,000(MINUS 1/0) · 경계 행이 없으면 100,000 = 100,000 · 시간대 -11:00 → +13:00: D-1 → D-1(어제의 답을 오늘도) · 다른 세션 · NLS · 직원까지 |
| 11:10 | (선배) 그 한 단어로 목록이 빨라진다 | 목록 정산일 본문 100,000(그냥) → 1(2019 DETERMINISTIC) → 0 · 찾기 1(금요일 판). 번 것이 없다 · 문장에 SYSDATE가 있으면 거절, 함수에 숨기면 받아 줌 · ENFORCED도 DETERMINISTIC은 믿는다 |
| 13:00 | 결과 캐시가 맞는 자리 | Q2 5,868 × 10 → 5,867 + 0 · 옆 세션 0(그러나 운영의 주문 테이블은 늘 커밋된다. 그래서 보류) · 예상 보정액 100,000 / 57,664 / 57,664 / 14,467 → 12,551 |
| 13:40 | 두 번째 판은 0일 것 | 결과 캐시 1,984칸 · 답 하나 = 칸 하나. 고객 2,000명이 안 들어간다 · 1,500명이면 0 |
| 14:20 | 지우는 규칙 | 커밋 전 남의 세션은 옛 답(맞다) · 같은 값이어도 지움 · ROLLBACK은 안 지움 · 한 명 고치면 1,500칸 전부 · 예외는 저장 안 함 |
| 15:10 | "아예 FORCE로" | 한 번 묻는 조회가 칸을 밀어냄 · 함수 안 SQL 39~41 → 61~69cs · 고치고 읽기 1,000 = 만듦 1,000 · 찾음 0 · 힌트 없이 시계가 멈춘다 |
| 15:50 | 보드 앞이 좁다 | 찾기 100만 번 혼자 20~21cs → 둘이 34~42cs · 래치 misses 수천 ~ 수만 · sleeps 0 |
| 16:30 | 원본으로 되돌렸으니 끝 | 거짓 약속이 남고 시험할 수 없다. 그래서 SDT_rev01(시각을 인자로 · 두 단어 제거 · 부르는 쪽 스칼라 서브쿼리) · 일곱 경계 · 1,440점 MINUS 0/0 · Buffers 549 → 549 |
왜 이렇게 짰을까? 15화까지 선배는 늘 옳았다. 막간에 선배가 틀리는 것은 이 교재의 규칙(측정, 경계, 원장 대조)이 선배의 판단보다 위에 있다는 걸 보여 주기 위해서다. 선배는 체크리스트를 다 지켰다. 경계 하나(시각)가 목록에 없었을 뿐이다.
오늘 배우는 한 문장
같은 답을 두 번 계산하지 마라. 다만 그 전에, 그게 정말 같은 답인지부터 물어야 한다. 결과 캐시는 인자와 테이블만 보고 '같은 질문'인지 판단한다. 시계, 세션 시간대, NLS, 세션 상태처럼 인자로 들어오지 않는 입력이 있으면 다른 질문에 같은 답을 준다. 그리고
DETERMINISTIC은 캐시가 아니라, 아무도 검사하지 않는 약속이다.
| 같은 일 | 무엇이 줄었나 | 언제 사라지나 · 누가 함께 |
|---|---|---|
| 버퍼 캐시 (Q2 × 10) | 디스크 읽기만. 매번 5,868블록을 다 읽는다 | LRU · 모든 세션 |
| Server Result Cache (Q2) | 계산 전부(5,867 → 0) | 의존 테이블 커밋 · 모든 세션 |
| PL/SQL Function Result Cache (예상 보정액) | 함수 100,000 → 14,467 / 12,551(칸 부족) · 1,500명이면 → 0 | 의존 테이블 커밋 · 재컴파일 · 밀려남 · 모든 세션 |
스칼라 서브쿼리 캐시 · DETERMINISTIC | 함수 100,000 → 57,664 | 문장 · 페치가 끝나면 · 그 실행 혼자 |
🔑 오늘의 비유 — 7층 복도의 화이트보드 「자주 묻는 것」
| 화이트보드 | 오라클 |
|---|---|
| 한 번 구한 답을 복도에 적어 두고, 층 전체가 지나가며 읽는다 | 결과 캐시: 계산이 끝난 답 · 인스턴스 전체 |
| 탕비실 픽업 선반의 캐리어(← 07화) | 버퍼 캐시: 재료(블록). 계산은 매번 다시 한다 |
| 내 책상 위 메모판(← 07화) | 스칼라 서브쿼리 캐시: 나 혼자, 한 문장 동안 |
| "규정집이 바뀌면 그 칸을 지웁니다" | 자동 무효화: 의존 객체가 바뀌어 커밋되면 지운다 |
| "오늘 등록하면 정산일은? → 6/14 (금)" | 시계에 기대는 함수의 답: 달력은 지우는 규칙에 없다 |
| 칸이 열두 개뿐이라 고객 2,000명은 못 적는다 | 1,984칸 · 답 하나 = 칸 하나 · 밀려남 |
| 규정집 한 줄이 바뀌면 그 규정집에 기대는 칸 전부를 지운다 | 고객 한 명 커밋 → 1,500칸 전부 Invalid |
| 보드 앞이 좁아서 둘이 동시에 보면 한 사람은 어깨 너머로 본다 | 결과 캐시 래치 · 둘이면 각자 두 배 가까이 |
| 질문지에 적힌 "답 안 바뀜". 아무도 확인하지 않는다 | DETERMINISTIC |
| 조민우 씨가 답을 지우고 규칙을 적었다: "18시 전 = 오늘 · 18시 후 = 내일" | SDT_rev01: 답을 저장하지 않고, 입력(시각)을 받아 규칙으로 계산한다 |
앞에서 나온 비유도 다시 나온다. 07화 탕비실 픽업 선반과 그 집게(래치, 메모리 읽기도 일이다), 07화 책상 위 메모판(스칼라 서브쿼리 캐시의 칸 = DETERMINISTIC의 칸 57,664), 15화 책상 위 등급표(지역 변수와 패키지 변수, 세션마다 한 벌)다.
☠️ 오늘의 함정 — 빠르지도 않은데 틀린 코드
시계를 읽는 함수에 RESULT_CACHE → 처음 부른 순간의 답이 인스턴스 전체에 · 기한 없이
"테이블이 바뀌면 오라클이 지운다" → 시계 · 세션 시간대 · NLS · 세션 상태는 테이블이 아니다
같은 날 오후에 대조했다 (건수 · 합계 · MINUS) → 100,000 = 100,000 — 정산일이 전부 과거라 '이 데이터에서 같았다'
DETERMINISTIC 이라고 적혀 있었다 → 약속일 뿐 — ENFORCED 조차 적힌 대로 믿는다
그 한 단어로 목록이 빨라졌나 → 2019 원본이 이미 1 번 — 번 것 0 · 잃은 것 정산일
계획 · Buffers · 계획 해시 → 셋 다 같다 (549 · 3721302985) — 4수치로 안 보인다
"두 번째는 0 이겠지" → 칸 1,984 · 답 하나 = 칸 하나 → 12,551
RESULT_CACHE_MODE = FORCE → 힌트 한 줄 없이 같은 사고느린 코드를 빠르게 만드는 것이 튜닝이고, 맞는 코드를 틀리게 만드는 것은 장애다. (← 02화) 오늘은 빨라지지도 않았다.
실습 준비
bash
docker start ora-hanorder15화를 마친 상태에서 시작한다. 실습/00_준비와_점검.sql이 이 상태를 확인한다(T_ORD_BIG 100만 · 인덱스 3 · ORD_ST 히스토그램 · T_ORD_STLM 10만 · T_STLM_NUM 0 · PKG_ORD_STLM · PKG_LOAD_DRIVER VALID · C15 0).
PowerShell에서는 먼저
$OutputEncoding = New-Object System.Text.UTF8Encoding $false를 한 줄 실행한다.
| 스크립트 | 무엇 | 계정 | 소요 |
|---|---|---|---|
실습/91_결과캐시_권한_SYSDBA.sql | DBMS_RESULT_CACHE 실행 권한(한 번) | SYS | 1초 |
실습/00_준비와_점검.sql | 전제 확인 · 복사본(C15B) · 함수 판들 · 수첩 · 옆 세션 연락판 | sqlp | 5초 |
실습/01_얼어붙은_정산일.sql → 01b_옆세션.sql | ★ 오늘의 사건 · 경계 시험 | sqlp | 15초 + 5초 |
실습/02_캐싱_4계층.sql | ★ 4계층 · 칸 · DETERMINISTIC 범위 · Bypass | sqlp + 일꾼 | 10초 |
실습/03_무효화.sql | 무효화 아홉 판 | sqlp + 일꾼 | 5초 |
실습/04_FORCE_세션.sql | FORCE(내 세션에서만) | sqlp | 5초 |
실습/05_경합.sql | 둘이 동시에 · 래치 | sqlp + 일꾼 | 5초 |
실습/일꾼.sql | '옆 세션'. 셸이 뒤에서 띄운다 · EXIT를 받거나 60초 동안 지시가 없으면 스스로 끝난다 | sqlp | — |
실습/93_결과캐시_무결성_SYSDBA.sql | (선택) ENFORCED · 끝에서 원복 | SYS | 5초 |
실습/99_정리.sql → 92_결과캐시_권한회수_SYSDBA.sql | C15B 삭제 · 결과 캐시 비우기 · 15화 종료 상태 확인 → 권한 회수 | sqlp → SYS | 수 초 |
bash
# 일꾼과 함께 돌리는 법 (02 · 03 · 05) — Git Bash
H="docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1"
$H < 실습/일꾼.sql > /dev/null & $H < 실습/02_캐싱_4계층.sql; wait모든 스크립트를 두 번씩 돌려 같은 판정이 나오는 것을 확인했다. 본문 실행 수, 찾기, 만들기, 건수는 같고 경과 시간과 래치 misses만 흔들린다. 칸이 밀려날 때 어느 결과가 밀려나는지는 판마다 다르다(해당하는 자리에 적었다). ⚠️ 스케줄러 잡은 쓰지 않는다(
job_queue_processes= 1). 옆 세션은 07, 11, 12화와 같은 방식으로 셸이 띄운다. ℹ️ 결과 캐시는 인스턴스 전체가 함께 쓰는 메모리다. 이 화의 실습은 실험마다DBMS_RESULT_CACHE.FLUSH로 캐시를 비운다. 연습 DB에서만 하는 일이다. 운영에서FLUSH를 하면 모든 세션의 캐시가 지워진다. ℹ️ 실습은 Docker로 띄운 Oracle Database Free 한 대에서 모두 끝난다. 결과 캐시는 Free에도 있다(8MB · 1,984칸). CPU가 2개라 경합은 두 세션까지만 재고, 대규모 EE 환경은 별권에서 다룬다.
이 화의 산출물
| 무엇 | 어디 |
|---|---|
SDT_rev00 · SDT_rev01: 정산일 함수(6/14 반영판 → 시각을 인자로) · 새 조각 줄 | 리비전/ |
| T1 단건 SQL 튜닝 리포트 11호 ★: 4수치로 안 보이는 사고 · 08화 정합성 체크리스트(E에 시각 · 세션) · 원인 칸에 판단 근거 | 3_실습 STEP 8 · T1 양식 |
「SQL 코딩 가이드」 조문 11: 결과 캐시 · 숨은 입력 · DETERMINISTIC 〔근거 1회〕 | SQL_코딩가이드.md |
이 화가 다음으로 넘기는 것
| 무엇 | 어디로 |
|---|---|
| Q2는 결과 캐시 없이 rev01 그대로(보류 사유: 의존 테이블인 주문의 커밋 빈도) · '캐시'라는 말이 넷 중 무엇을 가리키는지 | 16화: 처음으로 시스템 전체를 본다 |
| 보정률 함수 결과 캐시 재적용: 운영 결과 캐시 크기와 고객 수를 확인한 뒤(T1 11호 12번 ①) | 운영 확인 → 18화 표준 |
RESULT_CACHE가 붙은 함수 전수 점검 · 코딩 가이드 조문 11 승격 · 정합성 체크리스트 경계 목록의 '시각 · 세션' 줄 | 18화 |
| 수백 세션의 결과 캐시 경합 · RAC의 결과 캐시 | 별권 |
← 15화 「SQL은 다 고쳤는데 왜 느린가」 · 교재 전체 안내 · 다음 → 16화 「처음으로 시스템 전체를 본다」