테마
01 — 첫 고객사 ora-hanorder를 올린다
🗓️ 3월 2일 (토) 오후 · Docker는 돈다. 이제 ㈜한빛유통의 DB를 내 노트북에 올린다. 이 문서를 마치면 컨테이너 1대와 테이블 14개, 그리고
Buffers가 5천 대인 실행계획 한 장이 생긴다. 01화의 첫 숫자를 내 화면에서 보게 된다.소요 시간: 약 3분(실측 기동 1분 8초 + 적재 22초). 이미지 내려받기(2.9GB) 시간은 뺐다. 📌 모든 수치는 2026-09-01 실측값이다. 다르게 나오면
04_일상_운용과_문제해결.md를 본다.
1. 무엇을 만드나
이 교재의 실습 DB는 컨테이너 4대이고, 컨테이너 하나가 고객사 하나다. 오늘은 그중 첫 번째 하나만 만든다.
┌─ 트랙 L : 내 노트북 (Docker · Oracle 26ai Free) ───────────────────┐
│ │
│ ora-hanorder :1521 ㈜한빛유통 통합주문 HANORDER ← ★ 오늘 │
│ ora-burger :1522 인수한 버거 프랜차이즈 POS (04화 전) │
│ ora-dist :1523 본사 레거시 유통 마스터 (03화 전) │
│ ora-newshop :1524 신규 이커머스 플랫폼 (12화 전) │
│ │
└────────────────────────────────────────────────────────────────────┘
ora-hanorder하나만 있으면 01~13화를 진행할 수 있다. 나머지 셋은 해당 화에 이르렀을 때03_다른_고객사_세_곳.md를 보고 만든다. 한 대에 몰아넣지 않는 이유는고객사/README.md에 세 줄로 적어 두었다(한 줄로 줄이면 Free는 DB당 12GB 한도라서 나눠야 한다).
2. 이미지 받기 (한 번만)
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로 이름이 바뀌었다. 태그는 23이지만 실체는 26ai다).💡 그 태그가 없다는 오류가 나면 https://hub.docker.com/r/gvenzl/oracle-free/tags에서 지금 있는 패치 태그를 골라 이 문서의 태그를 바꾼다.
latest와23-slim은 쓰지 않는다.
받은 뒤 확인한다.
bash
docker image inspect gvenzl/oracle-free:23.26.2-slim --format '{{index .RepoDigests 0}}'3. 기동
3-1. 기동 함수 정의 (복붙) — 4대 전부 이 함수로 만든다
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 <이름>-data:/opt/oracle/oradata | 데이터파일을 볼륨에 둔다. 이 옵션을 빼면 docker rm 한 번에 데이터가 모두 사라진다. 반대로 볼륨이 있으면 컨테이너는 껍데기일 뿐이라 언제든 갈아 끼울 수 있다. 포트를 바꾸고 싶으면 docker rm으로 지운 뒤 같은 볼륨을 붙여 다시 run하면 데이터는 그대로다 |
--shm-size=1g | /dev/shm 크기. Docker 기본값 64MB로는 SGA를 잡지 못해 ORA-00845가 난다 |
-p <포트>:1521 | 호스트에서 SQL Developer나 SQLcl로 접속할 포트. 고객사마다 다르게 준다 |
-e APP_USER=… | 실습 계정을 자동으로 만든다. 단, 기본 권한만 있어서 §4가 필요하다 |
함수는 셸 창을 닫으면 사라진다. 나중에 다른 고객사를 만들 때 다시 붙여 넣는다(
~/.bashrc에 넣어 두면 편하다).
3-2. 기동
bash
launch_ora ora-hanorder 1521 sqlp
wait_ora ora-hanorderpowershell
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 준비 완료실측 1분 8초(빈 볼륨에 처음 올릴 때). 환경에 따라 1~5분 걸리고, 두 번째 기동부터는 수십 초면 된다. 이 사이에 접속하면
ORA-12514가 난다. 고장이 아니라 아직 준비 중이라는 뜻이다. 그래서 대충 기다리지 않고wait_ora로 준비가 끝났는지 판정한다.
4. 측정 권한 부여 ★ 빠뜨리면 실행계획을 못 본다
gvenzl 이미지가 만든 sqlp에는 기본 권한만 있다. 이대로는 DBMS_XPLAN.DISPLAY_CURSOR와 AUTOTRACE가 ORA-01031로 막힌다.
이 저장소의 고객사/L1_ora-hanorder/ 폴더로 가서 실행한다.
bash
cd 10_본편_튜너의_1년/00_자료준비/고객사/L1_ora-hanorder
MSYS_NO_PATHCONV=1 docker exec -i ora-hanorder sqlplus -s / as sysdba < 00_측정권한_부여.sqlpowershell
$OutputEncoding = New-Object System.Text.UTF8Encoding $false # ★ PowerShell 창마다 한 번 — 파이프로 넘긴 한글이 ? 로 깨지지 않게 (02 §4)
Set-Location 10_본편_튜너의_1년\00_자료준비\고객사\L1_ora-hanorder
Get-Content 00_측정권한_부여.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s / as sysdba스크립트가 하는 일은 넉 줄이다. FREEPDB1로 전환하고, SELECT_CATALOG_ROLE과 SELECT ANY DICTIONARY, ALTER SESSION을 부여한 뒤, QUOTA UNLIMITED ON USERS를 준다. ★ 권한을 준 뒤에는 sqlp 세션에 다시 접속해야 롤이 활성화된다(지금은 열어 둔 세션이 없으니 그냥 진행한다).
5. 데이터 적재 — 구 시스템과 신 시스템
같은 폴더에서 두 스크립트를 순서대로 실행한다. 파일을 컨테이너에 복사하지 않고 stdin으로 흘려보낸다.
bash
# 구 시스템 (스키마 A) — 실측 5.5초
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 01_구시스템_스키마A.sql
# 신 시스템 (스키마 B, T_ORD_BIG 100만) — 실측 16.5초
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 02_신시스템_스키마B.sqlpowershell
Get-Content 01_구시스템_스키마A.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1
Get-Content 02_신시스템_스키마B.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1두 스크립트 모두 끝에 자체 검증 행수를 찍는다. PROMPT *** 기대: … *** 줄과 그 위 표를 대조해 눈으로 확인하고 넘어간다.
⚠️ 함정 ① — 컨테이너 안에서
@스크립트로 돌리면 멈춘다 이 스크립트들은 끝에
EXIT가 없다. 컨테이너에docker cp로 넣고sqlplus … @/tmp/a.sql로 실행하면 스크립트를 다 돌린 뒤SQL>프롬프트에서 입력을 기다리며 멈춘다. 겉으로는 "적재가 오래 걸리는 것"처럼 보인다. 위의 stdin 방식(< 파일/Get-Content … |)은 파일 끝에서 자동으로 종료되므로 안전하다. 멈춘 건지 도는 건지 구분하는 법은02_접속과_실행_방법.md§5에 있다.
🐞 함정 ② —
;뒤에 같은 줄 주석이 있으면 그 문장이 실행되지 않는다 실측 중에
T_ORD_DET이 몇 번을 돌려도 0행이 됐다. 원인은 이 한 줄이었다.sqlWHERE d.no <= MOD(o.ORD_SEQ,3)+1; -- 주문당 1~3건
;뒤 같은 줄에 주석이 붙어 있으면 SQL*Plus가 문장 버퍼를 끝내지 못하고 다음 문장을 이어 붙인다(ORA-03405가 나거나 조용히 실행되지 않는다). 다른 테이블은 다 정상이고 그 문장만 빠지므로 알아채기 어렵다. 그래서 §6의 검증을 반드시 거쳐야 한다. 이 저장소의 스키마 B 스크립트는 이미 고쳐 두었다(주석을 앞 줄로 올렸다). 내 스크립트에 같은 문제가 있는지는 이렇게 찾는다.bashgrep -rn ";[[:space:]]\+--" <스크립트 경로>
6. 검증 — 이게 통과해야 끝난 것이다
bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 03_환경검증.sqlpowershell
Get-Content 03_환경검증.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1기대 (전부 일치해야 한다)
DEPT | 4 | M_RGN | 8 |
EMP | 14 | M_ITM | 100 |
SALGRADE | 5 | M_ITM_PRC_HIS | 100 |
PRODUCTS | 2,000 | M_CUS | 2,000 |
CUSTOMERS | 20,000 | T_ORD | 100,000 |
ORDERS | 200,000 | T_ORD_DET | 200,000 |
T_ORD_BIG | 1,000,000 | ||
used_mb | ≈ 95 | ||
T_ORD_BIG 인덱스 수 | 0 |
6-1. 마지막 관문 — 측정 도구가 실제로 도는가
검증 스크립트의 마지막 단계 [4]가 이 한 장을 찍는다.
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%다. 환경 구축을 마치는 순간 첫 사건이 이미 눈앞에 있다. 이 숫자는 01화에서 다시 본다.
- 표 대신
ORA-01031이 나오면 §4를 돌리지 않았거나, 돌리기 전에 연 세션이다. 다시 접속한다.- 표 대신
DBMS_OUTPUT.GET_LINES가 나오면SET SERVEROUTPUT ON상태다. 측정 전에는 항상SET SERVEROUTPUT OFF로 둔다.
숫자가 1~2블록 다르게 나온다면 — 정상이다
실측 (2026-10-04, 다른 PC): 같은 스크립트로
Buffers 5,826이 나왔다. 이 문서의 기준값은5,827이다. 풀 스캔은 HWM 아래 블록과 세그먼트 헤더, ASSM 비트맵 블록을 읽는데, 이 부가 블록의 수가 익스텐트 할당에 따라 한두 개 달라진다. (두 환경 모두user_tables.blocks = 5,923, 세그먼트 익스텐트 62개 / 47MB로 같았다.)같아야 하는 것은 숫자 하나가 아니라 다음 세 가지다. 오늘 배울 내용이 바로 여기에 있다.
TABLE ACCESS FULL(인덱스를 안 탄다)E-Rows 333KvsA-Rows 20,000(16배 오추정)Buffers가 5천 대 (결과 1행을 얻으려고 테이블 전체를 읽는다)자릿수가 다르거나 오퍼레이션이 다르면 그때는 진짜 문제다.
04_일상_운용과_문제해결.md를 본다.
7. 지금 무엇이 생겼나
한 계정(sqlp) 안에 시스템이 둘 있다. 01화 오전에 이 사실을 user_tables로 직접 확인하게 된다.
| 구 시스템 (스키마 A) | 신 시스템 (스키마 B) | |
|---|---|---|
| 만든 때 | 2015 | 2019, 강도현 |
| 이름 규칙 | DEPT EMP ORDERS … 영어 단어 | M_CUS T_ORD T_ORD_BIG … M_/T_ 접두어 |
| 규모 | ORDERS 20만 | T_ORD_BIG 100만 |
| 인덱스 | ORDERS에 3개 | T_ORD_BIG에 0개 (PK도 없다) |
| FK | EMP→DEPT 1개 | 0개 |
| 편향 | status 90/8/2 · grade 1/9/90 | ORD_ST 90/8/2 · CUS_GD 1/9/90 |
편향이 핵심이다. 값이 고르게 퍼져 있으면 히스토그램도, 손익분기점도, 바인드 피킹도 배울 수 없다. 이 데이터는 일부러 치우쳐 있고, 일부러 인덱스가 없다. ⚠️
M_CUS.CUS_ID는VARCHAR2('CUS_000123'),CUSTOMERS.cust_id는NUMBER다. 두 시스템을 한 쿼리에서 섞으면 안 된다.
테이블별 상세와 ERD는 고객사/L1_ora-hanorder/README.md에 있다. 차지하는 공간은 약 95MB로, 이 컨테이너의 12GB 예산 중 1%도 쓰지 않는다.
8. 다음
02_접속과_실행_방법.md:SQL>프롬프트를 여는 법을 익힌다. 01화의 SQL은 전부 이 프롬프트에 붙여 넣는다(10분).- → 01화 · 인수인계 없는 인수인계
컨테이너는 켜 둔 채로 둬도 되고, docker stop ora-hanorder로 껐다가 월요일에 docker start ora-hanorder로 다시 켜도 된다. 볼륨이 있으니 데이터는 그대로다.