테마
📌 보존 원본 (2026-09-01 실측 기록). 이 문서의 내용은
00_자료준비/의01_첫_고객사_ora-hanorder,03_다른_고객사_세_곳,04_일상_운용과_문제해결,05_Free와_EE_트랙G로 나누어 다시 썼다. 본편 독자는 그쪽을 보면 된다. 이 문서는 수치의 출처를 확인할 때 편다. 아래 링크의 경로는 이관 후 구조에 맞게 고쳤고, 본문 내용은 그대로 두었다.
00. 실습 환경 구축 가이드 (개정판)
이 문서 하나만 따라 하면 로컬 실습 환경이 완성된다. 다른 문서를 먼저 읽을 필요는 없다. 모든 명령은 복붙해서 쓰면 되고, 단계마다 정상일 때 나오는 출력을 적어 두었다. 다르게 나오면 §9를 본다.
소요 시간: L1(본편 무대)까지 약 3분(실측 기동 1분 8초 + 적재 22초, 이미지 내려받기 제외). L2~L4는 해당 화에 이르렀을 때 추가한다. 📌 모든 수치는 2026-09-01 실측값이다.
1. 이 문서대로 하면 무엇이 생기나
오라클 컨테이너 4대가 생기고, 컨테이너 하나하나가 고객사 시스템 하나다.
┌─ 트랙 L : 내 노트북 (Docker · Oracle 26ai Free) ───────────────────┐
│ │
│ ora-hanorder :1521 ㈜한빛유통 통합주문 HANORDER ← 본편 무대 │
│ ora-burger :1522 인수한 버거 프랜차이즈 POS │
│ ora-dist :1523 본사 레거시 유통 마스터 │
│ ora-newshop :1524 신규 이커머스 플랫폼 │
│ │
│ → SQL 작성 · 실행계획 판독 · 인덱스 설계 · 쿼리 재작성 │
└────────────────────────────────────────────────────────────────────┘
↓ 나중에
┌─ 트랙 G : Google Cloud (Oracle 19c Enterprise Edition) ────────────┐
│ → 병렬 실행 · 다세션 부하 · 대용량 배치 · 설치와 구성 │
└────────────────────────────────────────────────────────────────────┘왜 4대로 나누나? 세 줄로 요약하면 다음과 같다(자세한 근거는 부록 A).
- Oracle Free는 DB 하나당 사용자 데이터가 12GB까지다. 나누면 컨테이너마다 12GB 예산이 따로 생긴다.
- 필요한 것만 켠다. 오라클 한 대가 메모리를 1~1.5GB 쓰므로 4대를 늘 켜 둘 이유가 없다.
- 컨설턴트는 고객사마다 다른 DB에 접속한다. 현실에서는 한 계정에 여러 회사의 데이터가 섞여 있지 않다.
L1 하나만 만들어도 01~13화를 진행할 수 있다. L2~L4는 해당 화에 이르렀을 때 만들면 된다.
2. 준비물
| 항목 | 요구 | 확인 | 정상 출력 |
|---|---|---|---|
| Docker Desktop | 설치 + 실행 중 | docker version --format '{{.Server.Version}}' | 29.5.3 (숫자면 OK) |
| 디스크 여유 | L1만: 10GB / 4대 전부: 40GB | df -h /c | tail -1 | ... 58G 88% /c |
| 메모리 | 8GB 이상 (동시 1~2대 기동 기준) | — | — |
| 셸 | Git Bash · WSL · PowerShell 중 아무거나 | — | — |
이미지 받기 (한 번만)
bash
docker pull gvenzl/oracle-free:23.26.2-slim왜
23-slim이 아니라23.26.2-slim인가?23-slim은 이동 태그라서 받는 시점마다 내용물이 달라진다. 실측해 보니 3개월 전에 받은23-slim(IDfbbd30…)과 당일 받은23.26.2-slim(ID6c6b25…)이 서로 다른 이미지였다. 같이 공부하는 사람끼리 실습 결과가 맞지 않으면 SQL보다 여기부터 의심해야 한다. 패치 태그를 고정한다.이 저장소의 기준 이미지:
gvenzl/oracle-free@sha256:6c6b2555e19b81beb79f3429ac34f241b196785c0137125b871f0af8507685b1제품명은 Oracle AI Database 26ai Free Release 23.26.2.0.0이다(2025-10에23ai에서26ai로 이름이 바뀌었다).
3. 각 컨테이너에 무엇이 들어가나
각 컨테이너는 성격이 서로 다른 고객사 DB이고, 무엇을 배우는 데 쓰는지도 다르다.
L1 ora-hanorder — ㈜한빛유통 통합주문 HANORDER ★ 본편 무대
전체 19화 중 15화가 여기서 벌어진다. 계정은 sqlp이고, 두 스키마가 함께 들어간다.
① 스키마 A: 사원/주문 기본 세트 (SCOTT 확장판)
| 테이블 | 행수 | 성격 |
|---|---|---|
DEPT / EMP / SALGRADE | 4 / 14 / 5 | 오라클 고전 SCOTT. 개념 확인·조인 연습용 소량 |
PRODUCTS | 2,000 | 상품 |
CUSTOMERS | 20,000 | 고객. grade가 편향돼 있다(A 1% / B 9% / C 90%) |
ORDERS | 200,000 | 주문. status가 편향돼 있다(COMPLETE 90% / PENDING 8% / CANCEL 2%) |
편향이 핵심이다. 값이 고르게 퍼져 있으면 히스토그램도, 손익분기점도, 바인드 피킹도 배울 수 없다. 이 데이터는 일부러 치우쳐 있고, 일부러 인덱스가 없다.
② 스키마 B: 쇼핑몰 주문 (본편의 실제 무대)
| 테이블 | 행수 | 성격 |
|---|---|---|
M_RGN | 8 | 지역 코드 마스터 |
M_ITM | 100 | 상품 마스터 |
M_ITM_PRC_HIS | 100 | 가격 이력. (ITM_ID, TO_YMD) 최신 1건 조회 튜닝 소재 |
M_CUS | 2,000 | 고객. CUS_GD 등급 편향. CUS_ID가 VARCHAR2('CUS_000123') |
T_ORD | 100,000 | 주문 |
T_ORD_DET | 200,000 | 주문상세(주문당 1~3건) |
T_ITM_EVL | 5,000 | 상품평가. M:1:M 조인 폭증(매출이 평가 건수만큼 중복 합산) 재현용 |
T_ORD_BIG | 1,000,000 | 의도적 무인덱스. "선배가 인덱스 하나 안 만들고 떠난 테이블" |
⚠️
M_CUS.CUS_ID는 문자열이고 스키마 A의cust_id는 숫자다. 두 스키마를 한 쿼리에서 섞으면 안 된다. 두 스키마가 같은 컨테이너에 있는 것은 "같은 회사의 구·신 시스템"이라는 설정 때문이다.
공간: 약 95MB (12GB 한도의 1% 미만) ERD: 스키마A · 스키마B
L2 ora-burger — 인수한 버거 프랜차이즈 POS
계정은 dev다. 클러스터링 팩터(CF)와 파티션을 배우는 곳이며, 테이블은 17개다.
| 테이블 | 행수 | 왜 중요한가 |
|---|---|---|
ORD | 1,000,000 | 주문. 인덱스 CF ≈ 22,223 (테이블 블록 수와 비슷 = 잘 정렬됨) |
ORD_RANDOM | 1,000,000 | 컬럼 정의·행수·인덱스 정의가 ORD와 완전히 같다. 적재 순서만 다르다. CF ≈ 999,965 (행수와 비슷 = 완전히 흩어짐) |
ORD_ITEM / ORD_ITEM_RANDOM | 2,472,090 | 주문상세 버전 |
ORD_LIST · ORD_RANGE | 1,000,000 | LIST / RANGE 파티션 (각각 파티션 25개) |
ORD_LIST_HASH | 1,000,000 | LIST 25 × HASH 8 = 200 서브파티션 |
ORD_ITEM_LIST(_HASH) · ORD_ITEM_RANGE | 2,472,090 | 상세의 파티션 버전 |
ORD_TEMP / ORD_TEMP2 | 1,000,000 | 적재용 중간 스테이징. 다 쓰면 지워도 된다(합계 ~1GB, 2~4.sql로 재생성) |
SHOP · ITEM · UITEM · UITEM_PRICE · COM_CD | 7~260 | 코드·마스터 |
ORD와ORD_RANDOM의 대비가 이 컨테이너의 존재 이유다. 행수와 컬럼, 인덱스가 전부 같은데 CF만 45배 차이 난다. "같은 인덱스인데 왜 하나는 쓰고 하나는 안 쓰나"를 다른 변수 없이 순수하게 보여 주는 대조군이다. 이보다 나은 교보재는 만들 수 없다.⚠️ 이 컨테이너는 크다(약 11.5GB로 12GB 한도에 육박한다).
ORD_LIST_HASH하나가 100만 행에 3GB를 쓴다. 비파티션ORD는 같은 100만 행에 176MB이니 17배다. 거의 비어 있는 서브파티션 200개마다 최소 익스텐트가 잡히기 때문이다. 이것은 버그가 아니라 14화에서 잴 현상 그 자체다.
적재: 데이터생성기/오라클튜닝에센셜/README.md의 1.sql ~ 11.sql
L3 ora-dist — 본사 레거시 유통 마스터
계정은 pup이다. "진짜 레거시는 이렇게 생겼다"를 보는 곳이며, 테이블은 18개(CF01~CF18)다.
| 테이블 | 행수 | 성격 |
|---|---|---|
CF11 | 1,096,708 | 최대 테이블 |
CF10 | 500,000 | 대용량 |
CF01~CF09, CF12~CF18 | 100 ~ 수만 | 마스터·코드·이력 |
레거시의 물리적 실체가 여기에 다 있다.
- 날짜가
VARCHAR2(8)에'YYYYMMDD'문자열로 들어 있다. 03화에서 "인덱스가 원천 봉쇄되는" 사건의 유일한 증거다- 보조 인덱스가 0개다(PK만 있음). 인덱스를 직접 설계해 봐야 한다
- 컬럼명과 코드값에 한글이 있고, CSV는 CP949 인코딩이다
스키마 B의 깔끔한
DATE컬럼으로는 "문자로 박힌 날짜" 문제를 재현할 수 없다. 그래서 이 컨테이너가 따로 있다.
공간: 약 1GB (실측 세그먼트 0.16GB) 적재: 데이터생성기/오라클SQL파워업/README.md에 따라 data/data_csv.zip을 압축 해제한 뒤 sqlldr로 넣는다.
L4 ora-newshop — 신규 이커머스 플랫폼 + FK 원본
계정이 두 개 들어간다. Lock과 졸업 시험을 위한 컨테이너다.
| 스키마 | 내용 | 왜 필요한가 |
|---|---|---|
ORA_SQL_TEST | SQL BOOSTER 책 원본 테이블 9개 + FK 7개 + 채번 테이블 | 무인덱스 FK가 자식 테이블 전체에 락을 거는 현상은 FK가 실제로 걸려 있어야 재현된다. 다른 컨테이너에는 FK가 없다 |
ECOMMERCE | 3NF 정규화 테이블 30개, 약 48.5만 행. FK 0개, 보조 인덱스 0개 | 18화 졸업 과제. 그때까지 열어 보지 않는다(held-out). 처음 보는 스키마를 혼자 진단하는 것이 시험이다 |
💡
ORA_SQL_TEST(FK 7개)와ECOMMERCE(FK 0개)를 나란히 두면 "제약을 걸었을 때와 안 걸었을 때 락 범위가 어떻게 달라지는가"를 같은 컨테이너 안에서 비교할 수 있다.
적재: 데이터생성기/SQL_BOOSTER/README.md C1~C8 · 이커머스_데이터셋_구축가이드.md
요약표
| 컨테이너 | 포트 | 계정 | 크기 | 한 줄 | 주로 쓰는 화 |
|---|---|---|---|---|---|
ora-hanorder | 1521 | sqlp | ~95MB | 본편 무대. 편향된 데이터 + 무인덱스 100만 행 | 01~13, 15 |
ora-burger | 1522 | dev | ~11.5GB | CF 대조군과 파티션 폭증 | 04, 14 |
ora-dist | 1523 | pup | ~1GB | 문자열 날짜 레거시 | 03, 09 |
ora-newshop | 1524 | shop | ~2GB | FK 7개 + 졸업 과제 | 12, 18 |
공통 접속 정보 (모든 컨테이너 동일, 포트만 다름)
| 항목 | 값 |
|---|---|
| Host | localhost |
| PDB 서비스 | FREEPDB1 ← 평소 실습 |
| CDB 서비스 | FREE ← AWR 실습 |
| 관리자 | system / oracle · sys / oracle (as sysdba) |
| 실습 계정 | 위 표의 계정 (비밀번호 = 계정명) |
4. 만들기 — L1 본편 무대
여기까지만 해도 01~13화를 진행할 수 있다.
4-1. 기동 함수 정의 (복붙)
Git Bash / WSL
bash
launch_ora () { # 사용법: launch_ora <이름> <호스트포트> <앱계정>
local NAME=$1 PORT=$2 APPUSER=$3
docker volume create ${NAME}-data
docker run -d --name ${NAME} -p ${PORT}:1521 --shm-size=1g \
-v ${NAME}-data:/opt/oracle/oradata \
-e ORACLE_PASSWORD=oracle \
-e APP_USER=${APPUSER} -e APP_USER_PASSWORD=${APPUSER} \
gvenzl/oracle-free:23.26.2-slim
}
wait_ora () { # 기동 완료까지 대기
local NAME=$1
until docker logs ${NAME} 2>&1 | grep -q "DATABASE IS READY TO USE!"; do
echo " ... ${NAME} 기동 대기 $(date +%H:%M:%S)"; sleep 15
done
echo "${NAME} 준비 완료"
}PowerShell
powershell
function Launch-Ora($Name, $Port, $AppUser) {
docker volume create "$Name-data"
docker run -d --name $Name -p "${Port}:1521" --shm-size=1g `
-v "${Name}-data:/opt/oracle/oradata" `
-e ORACLE_PASSWORD=oracle -e "APP_USER=$AppUser" -e "APP_USER_PASSWORD=$AppUser" `
gvenzl/oracle-free:23.26.2-slim
}
function Wait-Ora($Name) {
do { Start-Sleep 15; $log = docker logs $Name 2>&1 | Out-String
Write-Host " ... $Name 기동 대기" } until ($log -match "DATABASE IS READY TO USE!")
Write-Host "$Name 준비 완료"
}
-v(볼륨)를 빼면 안 된다. 볼륨이 없으면docker rm한 번에 데이터가 모두 사라진다. 반대로 볼륨이 있으면 컨테이너는 껍데기일 뿐이라 언제든 갈아 끼울 수 있다. 포트를 바꾸고 싶으면docker rm으로 지운 뒤 같은 볼륨을 붙여 다시run하면 데이터는 그대로다.
4-2. 기동
bash
launch_ora ora-hanorder 1521 sqlp
wait_ora ora-hanorder정상 출력
ora-hanorder-data
db4e7bd48b15...
... ora-hanorder 기동 대기 13:12:20
... ora-hanorder 기동 대기 13:12:36
... ora-hanorder 기동 대기 13:12:54
... ora-hanorder 기동 대기 13:13:10
ora-hanorder 준비 완료
real 1m8.197s ← 빈 볼륨에 처음 올릴 때 실측실측 1분 8초. 환경에 따라 1~5분 걸리고, 두 번째 기동부터는 수십 초면 된다. 이 사이에 접속하면
ORA-12514가 난다. 고장이 아니라 아직 준비 중이라는 뜻이다. 그래서 대충 기다리지 않고wait_ora로 준비가 끝났는지 판정한다.
4-3. 측정 권한 부여 ★ 빠뜨리면 실행계획을 못 본다
bash
docker exec -i ora-hanorder bash -lc 'sqlplus -s / as sysdba' <<'SQL'
ALTER SESSION SET CONTAINER = FREEPDB1;
GRANT SELECT_CATALOG_ROLE, SELECT ANY DICTIONARY, ALTER SESSION TO sqlp;
ALTER USER sqlp QUOTA UNLIMITED ON USERS;
SQLgvenzl 이미지가 만드는
APP_USER에는 기본 권한만 있어서, 이대로는DBMS_XPLAN.DISPLAY_CURSOR와AUTOTRACE가ORA-01031로 막힌다. ★ 권한을 준 뒤 세션에 다시 접속해야 롤이 활성화된다.
4-4. 데이터 적재
저장소 루트에서 실행한다. 스크립트는 기존 것을 그대로 쓴다. 한 줄도 고치지 않았다.
bash
# 스키마 A (실측 5.5초)
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < "10_본편_튜너의_1년/00_자료준비/고객사/L1_ora-hanorder/01_구시스템_스키마A.sql"
# 스키마 B (T_ORD_BIG 100만 — 실측 16.5초)
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < "10_본편_튜너의_1년/00_자료준비/고객사/L1_ora-hanorder/02_신시스템_스키마B.sql"PowerShell: 같은 일을 한다.
powershell
Get-Content "10_본편_튜너의_1년\00_자료준비\고객사\L1_ora-hanorder\01_구시스템_스키마A.sql" -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1
Get-Content "10_본편_튜너의_1년\00_자료준비\고객사\L1_ora-hanorder\02_신시스템_스키마B.sql" -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1⚠️
printf "…\nEXIT\n" | sqlplus형태를 반드시 지킨다 — 그냥sqlplus @script로 하면 멈춘다 이 저장소의 적재 스크립트는 끝에
EXIT가 없다(마지막 줄이PROMPT *** 기대값 …). 그래서sqlplus -s ... @/tmp/a.sql로 실행하면 스크립트를 다 돌린 뒤SQL>프롬프트에서 입력을 기다리며 멈춘다. 겉으로는 "적재가 오래 걸리는 것"처럼 보여서 알아채기 어렵다.멈춘 건지 도는 건지 구분하는 법: 다른 셸에서 다음을 실행한다.
bashdocker exec -i ora-hanorder bash -lc 'sqlplus -s / as sysdba' <<'SQL' ALTER SESSION SET CONTAINER = FREEPDB1; SELECT sid, status, event, ROUND(last_call_et) et_sec FROM v$session WHERE username='SQLP' AND type='USER'; EXIT SQL
STATUS = INACTIVE이고EVENT = SQL*Net message from client면 다 끝났는데 프롬프트에서 대기 중이다. (ACTIVE에db file.../log file...계열 이벤트라면 진짜로 일하는 중이니 기다린다.)위의
printf ... | sqlplus형태는 스크립트를 실행한 뒤EXIT를 넣어 주므로 스크립트에EXIT가 있든 없든 안전하다.
두 스크립트 모두 끝에 자체 검증 결과를 찍는다. 반드시 눈으로 확인한다.
🐞 알아 둘 함정 —
;뒤에 같은 줄 주석이 있으면 그 문장이 실행되지 않는다 실측 중에
T_ORD_DET이 몇 번을 돌려도 0행이 됐다. 원인은 이 한 줄이었다.sqlWHERE d.no <= MOD(o.ORD_SEQ,3)+1; -- 주문당 1~3건
;뒤 같은 줄에 주석이 붙어 있으면 SQL*Plus가 문장 버퍼를 끝내지 못하고 다음 문장을 이어 붙인다. 그 결과ORA-03405: End of query reached; no additional text should follow.가 나거나, 조용히 실행되지 않는다. 다른 테이블은 다 정상이고 그 문장만 빠지므로 알아채기 어렵다. 그래서 §5의 검증을 반드시 거쳐야 한다.고치는 법: 주석을 앞 줄로 올리거나 다음 줄로 내린다(이 저장소의 스키마 B 스크립트는 이미 고쳐 두었다).
sql-- 주문당 1~3건 WHERE d.no <= MOD(o.ORD_SEQ,3)+1;같은 패턴이 남아 있는 곳: L2를 만들 때 걸릴 수 있다.
데이터생성기/오라클튜닝에센셜/1.sql의 16·17·26행이다. 실행 결과를 반드시 확인하고, 빠진 문장이 있으면 그 줄의 주석을 옮겨 다시 실행한다.내 스크립트에 같은 문제가 있는지는 이렇게 찾는다.
bashgrep -rn ";[[:space:]]\+--" <스크립트 경로>
5. 검증 — 이게 통과해야 끝난 것이다
bash
docker exec -i ora-hanorder bash -lc 'sqlplus -s sqlp/sqlp@localhost/FREEPDB1' <<'SQL'
SET LINESIZE 130 PAGESIZE 60 FEEDBACK OFF SERVEROUTPUT OFF
COL t FORMAT A14
SELECT 'DEPT' t, COUNT(*) c FROM DEPT UNION ALL SELECT 'EMP',COUNT(*) FROM EMP
UNION ALL SELECT 'SALGRADE',COUNT(*) FROM SALGRADE UNION ALL SELECT 'PRODUCTS',COUNT(*) FROM PRODUCTS
UNION ALL SELECT 'CUSTOMERS',COUNT(*) FROM CUSTOMERS UNION ALL SELECT 'ORDERS',COUNT(*) FROM ORDERS
UNION ALL SELECT 'M_RGN',COUNT(*) FROM M_RGN UNION ALL SELECT 'M_ITM',COUNT(*) FROM M_ITM
UNION ALL SELECT 'M_CUS',COUNT(*) FROM M_CUS UNION ALL SELECT 'T_ORD',COUNT(*) FROM T_ORD
UNION ALL SELECT 'T_ORD_DET',COUNT(*) FROM T_ORD_DET UNION ALL SELECT 'T_ITM_EVL',COUNT(*) FROM T_ITM_EVL
UNION ALL SELECT 'T_ORD_BIG',COUNT(*) FROM T_ORD_BIG;
SELECT ROUND(SUM(bytes)/1024/1024,0) AS used_mb FROM user_segments;
SQL기대 (전부 일치해야 한다)
DEPT | 4 | M_RGN | 8 |
EMP | 14 | M_ITM | 100 |
SALGRADE | 5 | M_CUS | 2,000 |
PRODUCTS | 2,000 | T_ORD | 100,000 |
CUSTOMERS | 20,000 | T_ORD_DET | 200,000 |
ORDERS | 200,000 | T_ITM_EVL | 5,000 |
T_ORD_BIG | 1,000,000 | ||
used_mb | ≈ 95 |
5-1. 마지막 관문 — 측정 도구가 실제로 도는가
bash
docker exec -i ora-hanorder bash -lc 'sqlplus -s sqlp/sqlp@localhost/FREEPDB1' <<'SQL'
SET SERVEROUTPUT OFF LINESIZE 150 PAGESIZE 60 FEEDBACK OFF
SELECT /*+ GATHER_PLAN_STATISTICS */ COUNT(*) FROM T_ORD_BIG WHERE ORD_ST = 'CANCEL';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT=>'ALLSTATS LAST'));
SQL정상 출력 (실측)
xplan
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.03 | 5827 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.03 | 5827 |
|* 2 | TABLE ACCESS FULL | T_ORD_BIG | 1 | 333K| 20000 |00:00:00.03 | 5827 |이 한 장이 01화의 베이스라인이다. 볼 것은 셋이다.
TABLE ACCESS FULL,Buffers 5,827, 그리고E-Rows 333KvsA-Rows 20,000, 16배 오추정이다. 옵티마이저는ORD_ST의 값 3개가 고르게 분포한다고 보고 100만/3 ≈ 333K로 추정했지만, 실제CANCEL은 2%다. 환경 구축을 마치는 순간 첫 사건이 이미 눈앞에 있다.⚠️
SET SERVEROUTPUT OFF를 빼면DBMS_OUTPUT.GET_LINES가 "마지막 커서"가 되어 엉뚱한 실행계획을 보여 준다.
6. L2 · L3 · L4 추가 (해당 화에 도달했을 때)
만드는 방법은 L1과 똑같다. 이름과 포트, 계정만 바꾸고 적재 스크립트가 다를 뿐이다.
bash
launch_ora ora-burger 1522 dev ; wait_ora ora-burger # 04·14화 전
launch_ora ora-dist 1523 pup ; wait_ora ora-dist # 03화 전
launch_ora ora-newshop 1524 shop ; wait_ora ora-newshop # 12·18화 전권한 부여(§4-3)도 컨테이너마다 계정명만 바꿔 똑같이 한 번씩 해 준다.
적재는 각 데이터셋 README를 따른다. §3의 컨테이너별 설명 끝에 링크가 있다.
ora-burger주의: 적재가 끝나면 약 11.5GB로 12GB 한도에 육박한다. 여유 공간이 필요하면 적재용 스테이징 테이블을 지운다(2~4.sql로 언제든 다시 만들 수 있다).sqlDROP TABLE DEV.ORD_TEMP PURGE; DROP TABLE DEV.ORD_TEMP2 PURGE;
7. 일상 운용
bash
docker start ora-hanorder # 오늘 볼 고객사만 켠다
docker stop ora-burger # 안 쓰는 건 끈다 (메모리 회수)
docker ps --format "{{.Names}}|{{.Status}}|{{.Ports}}"화별로 어느 컨테이너를 켜나
| 화 | 켤 것 | 왜 |
|---|---|---|
| 01·02 | ora-hanorder | 베이스라인, 인덱스 무력화 |
| 03 | ora-hanorder + ora-dist | 문자열 날짜는 pup에만 있다 |
| 04 | ora-hanorder + ora-burger | CF 대조군 |
| 05~11, 13, 15 | ora-hanorder | 본편 |
| 12 | ora-hanorder + ora-newshop | 무인덱스 FK는 ORA_SQL_TEST에만 있다 |
| 14 | ora-burger (+ 트랙 G) | 파티션은 로컬, 병렬은 EE에서만 |
| 16·17 | ora-hanorder (+ 트랙 G) | AWR은 로컬에서 가능, 부하 규모는 G |
| 18 | ora-newshop | 졸업 과제 |
로그 보기: 정상인데도 놀랄 수 있는 메시지가 있다.
Checkpoint not complete
Thread 1 advanced to log sequence 686 (LGWR switch)고장이 아니다. 리두 로그가 작아 로그 스위치가 잦다는 신호이고, 13화에서
log file sync와 함께 다룰 실제 소재다.
8. 트랙 L(Free) vs 트랙 G(EE) — 무엇이 다르고, 왜 EE를 나중에 하나
8-1. 실측 비교
| 트랙 L: 26ai Free (로컬) | 트랙 G: 19c EE (GCP) | |
|---|---|---|
| 사용자 데이터 | 12GB / DB (엔진 레벨 하드 한도) | 무제한 |
| CPU | 2 | VM 사양대로 (권장 4 vCPU) |
| 메모리 | 컨테이너 할당 (1~2GB SGA) | 16GB VM 기준 넉넉 |
| 비용 | 무료 | $300 크레딧, 서울 리전 약 $0.172/시간 |
| 라이선스 | Oracle Free Use Terms: 개인 학습 허용 | OTN: 개발·테스트·데모만. 업무 데이터 반입 금지 |
8-2. 기능 가용성 (실측 + 공식 매뉴얼)
| 기능 | Free | EE | 비고 |
|---|---|---|---|
| Partitioning | ✅ | ✅ | Free에서도 된다. 파티션 실습은 로컬에서 한다 |
| Diagnostics Pack (AWR·ASH·ADDM) | ✅ | 💰 유상 옵션 | 역전 주의: Free는 포함, EE는 별도 구매 |
| Tuning Pack (SQL Monitor·SQL Tuning Advisor) | ✅ | 💰 유상 옵션 | 〃 |
| Database In-Memory | ✅ | 💰 | |
| Advanced Compression | ⚠️ | 💰 | V$OPTION은 TRUE인데 Data Pump 압축은 막혀 있다 |
| Parallel query / DML | ❌ | ✅ | 트랙 G가 필요한 첫 번째 이유 |
| Real Application Clusters | ❌ | 💰 | 개념만 다룬다 |
| Real Application Testing | ❌ | 💰 | 다루지 않는다 |
🔑 가장 흔한 오해를 먼저 깬다
"AWR을 하려면 EE가 필요하다"는 말은 틀렸다. 로컬 Free에서는 이미 AWR이 돌고 있다. 실측해 보면
dba_hist_snapshot이 68건이고v$active_session_history도 살아 있으며,control_management_pack_access = DIAGNOSTIC+TUNING(기본값)이다. 오히려 EE 쪽이 진단팩을 따로 사야 한다. 계약 없이 EE 운영계에서awrrpt를 돌리면 라이선스 위반이다.옛 자료에서 "XE에선 AWR을 못 쓴다"고 하는 것은 구 XE(18c/21c) 이야기다. 그때는 사실이었지만 26ai Free는 다르다.
⚠️ 그리고
V$OPTION은 라이선스 판정 도구가 아니다. "바이너리에 링크되어 있는가"에만 답한다. 실제로Advanced Compression = TRUE인데도expdp compression=ALL은ORA-00439로 막혔다. 기능을 쓸 수 있는지는 선언된 값이 아니라 직접 실행해 봐야 안다.
8-3. 그래서 EE로는 무엇을 더 하나
| # | EE에서만 되는 것 | 어느 화 | 왜 로컬로는 안 되나 |
|---|---|---|---|
| 1 | 병렬 실행 내부: QC·Granule·Table Queue·PQ_DISTRIBUTE·Downgrade, V$PQ_TQSTAT·V$PX_SESSION | 14 | Free는 Parallel execution = FALSE다. 힌트를 줘도 에러 없이 직렬로 강등되므로 V$PQ_TQSTAT가 0행이다 |
| 2 | work area 등급 전환 대조: 같은 조인을 PGA 2GB와 16GB에서 돌려 optimal → onepass → multipass가 뒤집히는 것을 본다 | 06·09 | 로컬은 메모리가 고정이라 한쪽만 볼 수 있다 |
| 3 | 현실적인 경합 프로파일: 수십 세션 동시 부하에서의 대기이벤트 분포 | 16·17 | 2 CPU에서는 모두 CPU 대기로 뭉개져 프로파일이 왜곡된다 |
| 4 | 팩 없는 현장 재현 → Statspack: CMPA=NONE으로 내려 SE2/무팩 EE 환경을 흉내 내고 spcreate.sql로 진단 | 17 | Free는 팩이 기본으로 켜져 있어서 "없는 상태"를 만들 수 없다 |
| 5 | 대용량 배치: 12GB 한도 없이 수천만 행 적재·삭제 | 14 | 한도에 걸린다 |
| 6 | 설치와 구성 그 자체: runInstaller · dbca · netca · 리스너 · CDB/PDB 생성 | 트랙 G 전용 | 컨테이너는 이미 다 만들어진 상태로 온다. OCP 시험 범위이기도 하다 |
8-4. 왜 나중에 하나
- 01~13화는 로컬로 충분하다. 인덱스, 조인, 옵티마이저, 통계, Lock, Redo는 모두 Free에서 측정할 수 있다.
- 크레딧 시계가 돈다. 크레딧은 $300 / 90일이고, 서울 리전에서 상시 가동하면 약 2.4개월이면 바닥난다. 실습 준비가 안 된 상태로 켜 두면 정작 필요할 때 크레딧이 없다.
- 부하를 걸려면 부하 드라이버부터 있어야 한다. 드라이버는 15화에서 PL/SQL로 직접 만든다. 도구 없이 EE를 띄워 봐야 볼 것이 없다.
- 설치 학습은 기초가 있어야 의미가 있다. SGA, PGA, 리스너, CDB를 모르는 상태에서
dbca를 돌리면 그냥 클릭일 뿐이다.
정리: 트랙 L은 "고객사 DB를 받아 와 내 노트북에서 뜯어보는" 자리다. 트랙 G는 "고객사 운영 환경에 실제로 들어가 보는" 자리다. 순서가 뒤바뀌면 둘 다 남는 게 없다.
상세 절차는
트랙G_GCP_19cEE/G00~G06에 있다.
9. 문제 해결
| 증상 | 원인 | 조치 |
|---|---|---|
failed to connect to the docker API at npipe://… | Docker Desktop 꺼짐 | 실행한 뒤 1~2분 기다린다 |
ORA-12514: TNS:listener does not currently know of service | 기동 미완료 | wait_ora로 DATABASE IS READY TO USE!를 확인한다(최초 3~10분) |
ORA-01031: insufficient privileges (DISPLAY_CURSOR) | 앱 계정 권한 부족 | §4-3을 실행한 뒤 다시 접속 |
DISPLAY_CURSOR가 DBMS_OUTPUT.GET_LINES를 보여 줌 | SERVEROUTPUT ON | 측정 전에 SET SERVEROUTPUT OFF |
T_ORD_DET가 0행 | 일괄 실행 시 간헐 발생 | 아래 복구 SQL을 한 번 실행한다 |
ORA-12954: exceeds the maximum allowed database size of 12 GB | 한 컨테이너에 과적재 | 컨테이너를 나눈다. 임시로는 ORD_TEMP류를 DROP |
ORA-00439: feature not enabled: Dump File Data Compression | Free는 Data Pump 압축 불가 | compression= 옵션을 뺀다 |
ORA-00845: MEMORY_TARGET not supported | /dev/shm 부족 | --shm-size=1g 이상으로 다시 만든다 |
SQL*Loader-500: Unable to open file | docker cp로 넣은 파일의 소유자가 root | docker exec -u 0 <c> chown -R oracle:oinstall <경로> |
port is already allocated | 포트 충돌 | docker ps로 확인한 뒤 다른 포트 사용 |
| 컨테이너를 지웠더니 데이터가 사라짐 | 볼륨 없이 기동 | 볼륨 필수(§4-1). 스크립트로 다시 만든다 |
| 지웠는데 디스크가 줄지 않음 (Windows) | WSL2 VHDX 미축소 | Docker Desktop → Settings → Resources → Reclaim space |
T_ORD_DET 복구 SQL
sql
INSERT /*+ APPEND */ INTO T_ORD_DET
SELECT o.ORD_SEQ, d.no,
'ITM'||LPAD(MOD(o.ORD_SEQ + d.no, 100)+1,5,'0'),
MOD(o.ORD_SEQ, 5)+1,
ROUND(DBMS_RANDOM.VALUE(1000,100000),-2)
FROM T_ORD o,
(SELECT level no FROM dual CONNECT BY level <= 3) d
WHERE d.no <= MOD(o.ORD_SEQ,3)+1;
COMMIT;전부 지우고 다시 시작하려면
bash
docker stop ora-hanorder ora-burger ora-dist ora-newshop 2>/dev/null
docker rm ora-hanorder ora-burger ora-dist ora-newshop 2>/dev/null
docker volume rm ora-hanorder-data ora-burger-data ora-dist-data ora-newshop-data 2>/dev/null
# 이미지는 남겨 둔다 (다시 받을 필요 없음)부록 A — 왜 한 대에 몰아넣지 않는가
이전 구성에서는 Docker 한 대에 데이터셋 5종을 모두 올렸다. 그 결과는 이랬다.
ORA-12954: The request exceeds the maximum allowed database size of 12 GB.| 항목 | 실측 |
|---|---|
USERS 테이블스페이스 | 12.33GB (한도 초과) |
| 여유 | 634MB |
DEV 스키마 하나 | 11.53GB |
PUP / SQLP | 0.16 / 0.03GB |
DEV의 파티션 테이블 두 개가 각각 3GB씩 차지해 나머지를 굶겼고, 스키마 B(본편 무대)를 올릴 공간조차 없었다. 게다가 한도가 찬 Free DB에서는 expdp 백업조차 실패한다(Data Pump 마스터 테이블을 만들지 못한다).
나눈 뒤 본편 무대는 95MB로, 자기 컨테이너의 12GB 예산 중 1%도 쓰지 않는다.
부록 B — 이 문서와 기존 문서의 관계
| 문서 | 상태 |
|---|---|
| 이 문서 | 환경 구축의 단일 출발점 |
환경구축/Oracle_실습환경_구축가이드.md | 단일 컨테이너 구성. 환경 부분은 이 문서로 대체한다. 측정 3종 세트 설명은 유효 |
환경구축/00_데이터셋_종합안내.md | 데이터셋 내용·ERD는 유효. "어느 계정에" 부분만 이 문서 §3을 따른다 |
환경구축/데이터생성기/** | 전부 그대로. 한 줄도 바뀌지 않았다 |
트랙L_Docker_Free/L01_… | 컨테이너 1대의 심화(무손실 이관·트레이스 반출 등) |