Skip to content

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(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로 이름이 바뀌었다. 태그는 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-hanorder
powershell
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_측정권한_부여.sql
powershell
$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.sql
powershell
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행이 됐다. 원인은 이 한 줄이었다.

sql
WHERE  d.no <= MOD(o.ORD_SEQ,3)+1;     -- 주문당 1~3건

; 뒤 같은 줄에 주석이 붙어 있으면 SQL*Plus가 문장 버퍼를 끝내지 못하고 다음 문장을 이어 붙인다(ORA-03405가 나거나 조용히 실행되지 않는다). 다른 테이블은 다 정상이고 그 문장만 빠지므로 알아채기 어렵다. 그래서 §6의 검증을 반드시 거쳐야 한다. 이 저장소의 스키마 B 스크립트는 이미 고쳐 두었다(주석을 앞 줄로 올렸다). 내 스크립트에 같은 문제가 있는지는 이렇게 찾는다.

bash
grep -rn ";[[:space:]]\+--" <스크립트 경로>

6. 검증 — 이게 통과해야 끝난 것이다 ​

bash
docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1 < 03_환경검증.sql
powershell
Get-Content 03_환경검증.sql -Encoding UTF8 | docker exec -i ora-hanorder sqlplus -s sqlp/sqlp@localhost/FREEPDB1

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

DEPT4M_RGN8
EMP14M_ITM100
SALGRADE5M_ITM_PRC_HIS100
PRODUCTS2,000M_CUS2,000
CUSTOMERS20,000T_ORD100,000
ORDERS200,000T_ORD_DET200,000
T_ORD_BIG1,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 333K vs A-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 333K vs A-Rows 20,000 (16배 오추정)
  • Buffers가 5천 대 (결과 1행을 얻으려고 테이블 전체를 읽는다)

자릿수가 다르거나 오퍼레이션이 다르면 그때는 진짜 문제다. 04_일상_운용과_문제해결.md를 본다.


7. 지금 무엇이 생겼나 ​

한 계정(sqlp) 안에 시스템이 둘 있다. 01화 오전에 이 사실을 user_tables로 직접 확인하게 된다.

구 시스템 (스키마 A)신 시스템 (스키마 B)
만든 때20152019, 강도현
이름 규칙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도 없다)
FKEMP→DEPT 1개0개
편향status 90/8/2 · grade 1/9/90ORD_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. 다음 ​

  1. 02_접속과_실행_방법.md: SQL> 프롬프트를 여는 법을 익힌다. 01화의 SQL은 전부 이 프롬프트에 붙여 넣는다(10분).
  2. → 01화 · 인수인계 없는 인수인계

컨테이너는 켜 둔 채로 둬도 되고, docker stop ora-hanorder로 껐다가 월요일에 docker start ora-hanorder로 다시 켜도 된다. 볼륨이 있으니 데이터는 그대로다.