Skip to content

📌 보존 원본 (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).

  1. Oracle Free는 DB 하나당 사용자 데이터가 12GB까지다. 나누면 컨테이너마다 12GB 예산이 따로 생긴다.
  2. 필요한 것만 켠다. 오라클 한 대가 메모리를 1~1.5GB 쓰므로 4대를 늘 켜 둘 이유가 없다.
  3. 컨설턴트는 고객사마다 다른 DB에 접속한다. 현실에서는 한 계정에 여러 회사의 데이터가 섞여 있지 않다.

L1 하나만 만들어도 01~13화를 진행할 수 있다. L2~L4는 해당 화에 이르렀을 때 만들면 된다.


2. 준비물 ​

항목요구확인정상 출력
Docker Desktop설치 + 실행 중docker version --format '{{.Server.Version}}'29.5.3 (숫자면 OK)
디스크 여유L1만: 10GB / 4대 전부: 40GBdf -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(ID fbbd30…)과 당일 받은 23.26.2-slim(ID 6c6b25…)이 서로 다른 이미지였다. 같이 공부하는 사람끼리 실습 결과가 맞지 않으면 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 / SALGRADE4 / 14 / 5오라클 고전 SCOTT. 개념 확인·조인 연습용 소량
PRODUCTS2,000상품
CUSTOMERS20,000고객. grade가 편향돼 있다(A 1% / B 9% / C 90%)
ORDERS200,000주문. status가 편향돼 있다(COMPLETE 90% / PENDING 8% / CANCEL 2%)

편향이 핵심이다. 값이 고르게 퍼져 있으면 히스토그램도, 손익분기점도, 바인드 피킹도 배울 수 없다. 이 데이터는 일부러 치우쳐 있고, 일부러 인덱스가 없다.

② 스키마 B: 쇼핑몰 주문 (본편의 실제 무대)

테이블행수성격
M_RGN8지역 코드 마스터
M_ITM100상품 마스터
M_ITM_PRC_HIS100가격 이력. (ITM_ID, TO_YMD) 최신 1건 조회 튜닝 소재
M_CUS2,000고객. CUS_GD 등급 편향. CUS_ID가 VARCHAR2('CUS_000123')
T_ORD100,000주문
T_ORD_DET200,000주문상세(주문당 1~3건)
T_ITM_EVL5,000상품평가. M:1:M 조인 폭증(매출이 평가 건수만큼 중복 합산) 재현용
T_ORD_BIG1,000,000의도적 무인덱스. "선배가 인덱스 하나 안 만들고 떠난 테이블"

⚠️ M_CUS.CUS_ID는 문자열이고 스키마 A의 cust_id는 숫자다. 두 스키마를 한 쿼리에서 섞으면 안 된다. 두 스키마가 같은 컨테이너에 있는 것은 "같은 회사의 구·신 시스템"이라는 설정 때문이다.

공간: 약 95MB (12GB 한도의 1% 미만) ERD: 스키마A · 스키마B


L2 ora-burger — 인수한 버거 프랜차이즈 POS ​

계정은 dev다. 클러스터링 팩터(CF)와 파티션을 배우는 곳이며, 테이블은 17개다.

테이블행수왜 중요한가
ORD1,000,000주문. 인덱스 CF ≈ 22,223 (테이블 블록 수와 비슷 = 잘 정렬됨)
ORD_RANDOM1,000,000컬럼 정의·행수·인덱스 정의가 ORD와 완전히 같다. 적재 순서만 다르다. CF ≈ 999,965 (행수와 비슷 = 완전히 흩어짐)
ORD_ITEM / ORD_ITEM_RANDOM2,472,090주문상세 버전
ORD_LIST · ORD_RANGE1,000,000LIST / RANGE 파티션 (각각 파티션 25개)
ORD_LIST_HASH1,000,000LIST 25 × HASH 8 = 200 서브파티션
ORD_ITEM_LIST(_HASH) · ORD_ITEM_RANGE2,472,090상세의 파티션 버전
ORD_TEMP / ORD_TEMP21,000,000적재용 중간 스테이징. 다 쓰면 지워도 된다(합계 ~1GB, 2~4.sql로 재생성)
SHOP · ITEM · UITEM · UITEM_PRICE · COM_CD7~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)다.

테이블행수성격
CF111,096,708최대 테이블
CF10500,000대용량
CF01~CF09, CF12~CF18100 ~ 수만마스터·코드·이력

레거시의 물리적 실체가 여기에 다 있다.

  • 날짜가 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_TESTSQL BOOSTER 책 원본 테이블 9개 + FK 7개 + 채번 테이블무인덱스 FK가 자식 테이블 전체에 락을 거는 현상은 FK가 실제로 걸려 있어야 재현된다. 다른 컨테이너에는 FK가 없다
ECOMMERCE3NF 정규화 테이블 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-hanorder1521sqlp~95MB본편 무대. 편향된 데이터 + 무인덱스 100만 행01~13, 15
ora-burger1522dev~11.5GBCF 대조군과 파티션 폭증04, 14
ora-dist1523pup~1GB문자열 날짜 레거시03, 09
ora-newshop1524shop~2GBFK 7개 + 졸업 과제12, 18

공통 접속 정보 (모든 컨테이너 동일, 포트만 다름)

항목값
Hostlocalhost
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;
SQL

gvenzl 이미지가 만드는 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> 프롬프트에서 입력을 기다리며 멈춘다. 겉으로는 "적재가 오래 걸리는 것"처럼 보여서 알아채기 어렵다.

멈춘 건지 도는 건지 구분하는 법: 다른 셸에서 다음을 실행한다.

bash
docker 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행이 됐다. 원인은 이 한 줄이었다.

sql
WHERE  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행이다. 실행 결과를 반드시 확인하고, 빠진 문장이 있으면 그 줄의 주석을 옮겨 다시 실행한다.

내 스크립트에 같은 문제가 있는지는 이렇게 찾는다.

bash
grep -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

기대 (전부 일치해야 한다)

DEPT4M_RGN8
EMP14M_ITM100
SALGRADE5M_CUS2,000
PRODUCTS2,000T_ORD100,000
CUSTOMERS20,000T_ORD_DET200,000
ORDERS200,000T_ITM_EVL5,000
T_ORD_BIG1,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 333K vs A-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로 언제든 다시 만들 수 있다).

sql
DROP 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·02ora-hanorder베이스라인, 인덱스 무력화
03ora-hanorder + ora-dist문자열 날짜는 pup에만 있다
04ora-hanorder + ora-burgerCF 대조군
05~11, 13, 15ora-hanorder본편
12ora-hanorder + ora-newshop무인덱스 FK는 ORA_SQL_TEST에만 있다
14ora-burger (+ 트랙 G)파티션은 로컬, 병렬은 EE에서만
16·17ora-hanorder (+ 트랙 G)AWR은 로컬에서 가능, 부하 규모는 G
18ora-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 (엔진 레벨 하드 한도)무제한
CPU2VM 사양대로 (권장 4 vCPU)
메모리컨테이너 할당 (1~2GB SGA)16GB VM 기준 넉넉
비용무료$300 크레딧, 서울 리전 약 $0.172/시간
라이선스Oracle Free Use Terms: 개인 학습 허용OTN: 개발·테스트·데모만. 업무 데이터 반입 금지

8-2. 기능 가용성 (실측 + 공식 매뉴얼) ​

기능FreeEE비고
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_SESSION14Free는 Parallel execution = FALSE다. 힌트를 줘도 에러 없이 직렬로 강등되므로 V$PQ_TQSTAT가 0행이다
2work area 등급 전환 대조: 같은 조인을 PGA 2GB와 16GB에서 돌려 optimal → onepass → multipass가 뒤집히는 것을 본다06·09로컬은 메모리가 고정이라 한쪽만 볼 수 있다
3현실적인 경합 프로파일: 수십 세션 동시 부하에서의 대기이벤트 분포16·172 CPU에서는 모두 CPU 대기로 뭉개져 프로파일이 왜곡된다
4팩 없는 현장 재현 → Statspack: CMPA=NONE으로 내려 SE2/무팩 EE 환경을 흉내 내고 spcreate.sql로 진단17Free는 팩이 기본으로 켜져 있어서 "없는 상태"를 만들 수 없다
5대용량 배치: 12GB 한도 없이 수천만 행 적재·삭제14한도에 걸린다
6설치와 구성 그 자체: runInstaller · dbca · netca · 리스너 · CDB/PDB 생성트랙 G 전용컨테이너는 이미 다 만들어진 상태로 온다. OCP 시험 범위이기도 하다

8-4. 왜 나중에 하나 ​

  1. 01~13화는 로컬로 충분하다. 인덱스, 조인, 옵티마이저, 통계, Lock, Redo는 모두 Free에서 측정할 수 있다.
  2. 크레딧 시계가 돈다. 크레딧은 $300 / 90일이고, 서울 리전에서 상시 가동하면 약 2.4개월이면 바닥난다. 실습 준비가 안 된 상태로 켜 두면 정작 필요할 때 크레딧이 없다.
  3. 부하를 걸려면 부하 드라이버부터 있어야 한다. 드라이버는 15화에서 PL/SQL로 직접 만든다. 도구 없이 EE를 띄워 봐야 볼 것이 없다.
  4. 설치 학습은 기초가 있어야 의미가 있다. 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 CompressionFree는 Data Pump 압축 불가compression= 옵션을 뺀다
ORA-00845: MEMORY_TARGET not supported/dev/shm 부족--shm-size=1g 이상으로 다시 만든다
SQL*Loader-500: Unable to open filedocker cp로 넣은 파일의 소유자가 rootdocker 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 / SQLP0.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대의 심화(무손실 이관·트레이스 반출 등)