Skip to content

스키마 B — SQL BOOSTER 쇼핑몰 주문 : ERD·조감도·인덱스 설명서 ​

계정: sqlp · 만드는 방법: 실습/00_SQLBOOSTER_실습스키마_setup.sql을 sqlp 계정으로 실행한다. 원서의 15GB 데이터 대신 CONNECT BY로 만든 축소판을 직접 생성한다. 도메인은 쇼핑몰 주문이다(지역, 상품, 고객, 주문, 주문상세, 상품평가 + 대용량 주문). 컬럼명과 타입은 원서 DDL을 그대로 따랐다.


1. ERD ​

mermaid
erDiagram
    M_RGN ||--o{ M_CUS : "1:N (RGN_ID)"
    M_CUS ||--o{ T_ORD : "1:N (CUS_ID)"
    M_CUS ||--o{ T_ITM_EVL : "1:N (CUS_ID)"
    T_ORD ||--o{ T_ORD_DET : "1:N (ORD_SEQ)"
    M_ITM ||--o{ T_ORD_DET : "1:N (ITM_ID)"
    M_ITM ||--o{ T_ITM_EVL : "1:N (ITM_ID)"
    M_ITM ||--o{ M_ITM_PRC_HIS : "1:N (ITM_ID)"

    M_RGN {
        VARCHAR2 RGN_ID PK
        VARCHAR2 RGN_NM
        NUMBER SRT_OD
    }
    M_ITM {
        VARCHAR2 ITM_ID PK
        VARCHAR2 ITM_NM
        VARCHAR2 ITM_TP
        NUMBER UNT_PRC
    }
    M_ITM_PRC_HIS {
        VARCHAR2 ITM_ID PK
        VARCHAR2 TO_YMD PK
        VARCHAR2 FR_YMD
        NUMBER UNT_PRC
    }
    M_CUS {
        VARCHAR2 CUS_ID PK
        VARCHAR2 CUS_NM
        VARCHAR2 RGN_ID
        VARCHAR2 GND_TP
        VARCHAR2 BTH_YMD
        VARCHAR2 CUS_GD
    }
    T_ORD {
        NUMBER ORD_SEQ PK
        VARCHAR2 CUS_ID
        DATE ORD_DT
        VARCHAR2 ORD_ST
        DATE PAY_DT
        NUMBER ORD_AMT
    }
    T_ORD_DET {
        NUMBER ORD_SEQ PK
        NUMBER ORD_DET_NO PK
        VARCHAR2 ITM_ID
        NUMBER ORD_QTY
        NUMBER UNT_PRC
    }
    T_ITM_EVL {
        VARCHAR2 ITM_ID PK
        NUMBER EVL_LST_NO PK
        VARCHAR2 CUS_ID
        DATE EVL_DT
        NUMBER EVL_PT
    }
    T_ORD_BIG {
        NUMBER ORD_SEQ
        VARCHAR2 CUS_ID
        DATE ORD_DT
        VARCHAR2 ORD_ST
        NUMBER ORD_AMT
    }

중요: 모든 관계는 논리적 관계다(FK 없음). DDL에는 테이블마다 PRIMARY KEY 제약만 있고, 외래키(FOREIGN KEY) 제약은 하나도 선언하지 않았다. 위의 1:N 선은 컬럼명(조인 키)이 같아서 데이터상으로 이어지는 논리 관계일 뿐이고, DB가 참조 무결성을 강제하지는 않는다.

T_ORD_BIG에는 어떤 키로도 연결선이 없다. PK조차 없는 독립(고립) 대용량 테이블이다. CUS_ID 값은 M_CUS와 같은 패턴이지만 제약도 인덱스도 전혀 없어서, 인덱스와 조인 성능 실습에만 쓴다.

텍스트(ASCII) 조감도 ​

                         [ M_RGN ]  지역 8건
                         RGN_ID(PK)
                              │ 1:N (RGN_ID)
                              ▼
      [ M_ITM ] 상품 100건            [ M_CUS ] 고객 2,000건
      ITM_ID(PK)                      CUS_ID(PK)  (VARCHAR2! 'CUS_000123')
        │   │   │                       │            CUS_GD = A(1%)/B(9%)/C(90%)
        │   │   └────────────┐          │
        │   │ 1:N(ITM_ID)    1:N(ITM_ID)│ 1:N (CUS_ID)
        │   ▼                 ▼          ▼
        │ [M_ITM_PRC_HIS]  [ T_ITM_EVL ]   [ T_ORD ] 주문 100,000건
        │  ITM_ID,TO_YMD    ITM_ID,         ORD_SEQ(PK)
        │  (PK) 가격이력      EVL_LST_NO(PK)  ORD_ST = COMPLETE/PENDING/CANCEL
        │  100건             평가 5,000건       │ 1:N (ORD_SEQ)
        │                                       ▼
        │ 1:N (ITM_ID)                   [ T_ORD_DET ] 주문상세 ~200,000건
        └──────────────────────────────► ORD_SEQ,ORD_DET_NO(PK)
                                          (주문당 1~3건)

      ┌─────────────────────────────────────────────────────┐
      │ [ T_ORD_BIG ] 대용량 주문 1,000,000건                  │
      │  ★ PK 없음 · 인덱스 전혀 없음 · 모든 관계 미연결(고립)    │
      │  CUS_ID VARCHAR2 → 6·7장 인덱스/조인 성능 실습 전용       │
      └─────────────────────────────────────────────────────┘

2. 테이블 조감도 ​

테이블의미PK대략 행수핵심 컬럼관계
M_RGN지역(마스터)RGN_ID8RGN_ID(VARCHAR2 'RGN01'), RGN_NM(SEOUL…JEJU), SRT_ODM_CUS.RGN_ID의 부모(논리)
M_ITM상품(마스터)ITM_ID100ITM_ID('ITM00001'), ITM_NM, ITM_TP(TP_A~D), UNT_PRCT_ORD_DET / T_ITM_EVL / M_ITM_PRC_HIS의 부모(논리)
M_ITM_PRC_HIS상품 가격이력ITM_ID, TO_YMD100TO_YMD(VARCHAR2(8) '20231231'), FR_YMD, UNT_PRCM_ITM.ITM_ID의 자식(논리)
M_CUS고객(마스터)CUS_ID2,000CUS_ID VARCHAR2 'CUS_000123', RGN_ID, GND_TP(M/F), BTH_YMD, CUS_GD A/B/CM_RGN의 자식 / T_ORD · T_ITM_EVL의 부모(논리)
T_ORD주문ORD_SEQ100,000ORD_SEQ(NUMBER), CUS_ID, ORD_DT(DATE), ORD_ST, PAY_DT, PAY_TP, ORD_AMT, PAY_AMTM_CUS의 자식 / T_ORD_DET의 부모(논리)
T_ORD_DET주문상세ORD_SEQ, ORD_DET_NO~200,000ITM_ID, ORD_QTY, UNT_PRC(주문당 1~3건)T_ORD · M_ITM의 자식(논리)
T_ITM_EVL상품평가ITM_ID, EVL_LST_NO5,000CUS_ID, EVL_DSC, EVL_DT(DATE), EVL_PTM_ITM · M_CUS의 자식(논리)
T_ORD_BIG대용량 주문없음(★)1,000,000ORD_SEQ, CUS_ID(VARCHAR2), ORD_DT, ORD_ST, ORD_AMT연결 없음(고립). 인덱스 실습용

데이터 분포 메모(DDL 기준):

  • CUS_GD 편향: A(MOD 100=0) 1% · B(MOD 10=0) 9% · C 90%. 등급 조건마다 선택도가 얼마나 다른지 실습한다.
  • ORD_ST 편향: CANCEL(MOD 50=0) 2% · PENDING(MOD 10=0) ~8% · COMPLETE 90%. 상태 조건도 한쪽으로 치우쳐 있다.
  • ORD_DT 분포: T_ORD는 2023-01-01부터 730일(2년), T_ORD_BIG는 2022-01-01부터 1095일(3년)에 걸쳐 있다.
  • CUS_ID 타입 주의: 스키마 B는 VARCHAR2(40)이고 'CUS_000123' 형식이다. 스키마 A의 cust_id(NUMBER)와 다르므로 조인이나 형변환을 실습할 때 헷갈리면 안 된다.

3. 인덱스·키 & 튜닝 포인트 ​

현재 존재하는 인덱스(= PK가 만든 고유 인덱스뿐) ​

DDL에는 따로 만드는 CREATE INDEX가 하나도 없다. PRIMARY KEY 제약이 자동으로 만든 고유 인덱스만 있다.

테이블자동 생성 인덱스(PK)컬럼 구성
M_RGNPK_M_RGN(RGN_ID)
M_ITMPK_M_ITM(ITM_ID)
M_ITM_PRC_HISPK_M_ITM_PRC_HIS(ITM_ID, TO_YMD) 복합
M_CUSPK_M_CUS(CUS_ID)
T_ORDPK_T_ORD(ORD_SEQ)
T_ORD_DETPK_T_ORD_DET(ORD_SEQ, ORD_DET_NO) 복합
T_ITM_EVLPK_T_ITM_EVL(ITM_ID, EVL_LST_NO) 복합
T_ORD_BIG없음(PK · 인덱스 전무)

의도적으로 "없는" 것 → 실습으로 직접 만든다 ​

  • T_ORD_BIG(1,000,000건): PK도 인덱스도 일부러 비워 두었다. 6장과 7장 실습에서 인덱스를 직접 만들고, Full Scan과 Index Range Scan의 Buffers와 Cost가 전후로 어떻게 달라지는지 비교한다.
    • 단일/복합 인덱스 후보: (CUS_ID), (ORD_DT), (ORD_ST, ORD_DT), (CUS_ID, ORD_DT) 등.
  • 외래키(FK) 없음: 부모-자식 관계가 모두 논리적이다. FK가 없으면 옵티마이저가 조인 제거(Join Elimination) 같은 일부 변환을 하지 못하므로, FK가 있고 없음에 따라 실행계획이 어떻게 달라지는지도 실습한다.
  • 자식 쪽 조인 키에 인덱스 없음: T_ORD.CUS_ID, T_ORD_DET.ITM_ID, T_ITM_EVL.CUS_ID 등에는 인덱스가 없다. NL 조인에서 자식 쪽 탐색이 Full Scan으로 풀리는 현상을 관찰하고, 인덱스를 추가하기 전과 후를 비교한다.

무엇을 실습하나 (테마) ​

  1. 데이터 편향(Skew): CUS_GD(A/B/C)와 ORD_ST(COMPLETE/PENDING/CANCEL)의 극단적인 분포로 히스토그램, 바인드 변수 Peeking, 선택도 오판을 실습한다.
  2. 클러스터링 팩터(CF): ORD_SEQ는 INSERT 순서(=물리 순서)와 정렬 순서가 같아서 CF가 좋다. CUS_ID나 ORD_DT로 만든 인덱스는 CF가 흩어져 있다. 이 둘의 인덱스 효율 차이를 실습한다.
  3. 대용량 처리(T_ORD_BIG): Full Scan과 인덱스를 비교하고, 파티션 없이 100만 행을 스캔해 보고, 인덱스를 추가한 효과를 측정한다.
  4. 복합키·결합 인덱스: T_ORD_DET(ORD_SEQ, ORD_DET_NO)와 M_ITM_PRC_HIS(ITM_ID, TO_YMD)에서 선두 컬럼을 조건에 쓰는지에 따라 액세스가 어떻게 달라지는지 본다.
  5. VARCHAR2 키 조인: 'CUS_000123' 같은 문자 키로 조인하거나 범위 조건을 걸 때의 동작과 묵시적 형변환의 위험을 본다(스키마 A의 NUMBER 키와 비교).
  6. 계층/이력 처리: M_ITM_PRC_HIS의 기간(FR_YMD~TO_YMD)으로 특정 시점의 단가를 찾는 이력 조회 패턴을 익힌다.

4. DB에서 확인하는 쿼리 ​

sql
-- (1) 테이블 목록·행수·블록수 (통계는 setup 의 GATHER_TABLE_STATS 로 수집됨)
SELECT table_name, num_rows, blocks, avg_row_len
FROM   user_tables
WHERE  table_name IN ('M_RGN','M_ITM','M_ITM_PRC_HIS','M_CUS',
                      'T_ORD','T_ORD_DET','T_ITM_EVL','T_ORD_BIG')
ORDER  BY num_rows;

-- (2) 컬럼 구성·타입·NULL 여부
SELECT table_name, column_name, data_type, data_length, data_precision, data_scale, nullable
FROM   user_tab_columns
WHERE  table_name IN ('M_RGN','M_ITM','M_ITM_PRC_HIS','M_CUS',
                      'T_ORD','T_ORD_DET','T_ITM_EVL','T_ORD_BIG')
ORDER  BY table_name, column_id;

-- (3) 제약(PK 등) — FK가 정말 없는지 확인 (R = 외래키)
SELECT table_name, constraint_name, constraint_type, status
FROM   user_constraints
WHERE  table_name IN ('M_RGN','M_ITM','M_ITM_PRC_HIS','M_CUS',
                      'T_ORD','T_ORD_DET','T_ITM_EVL','T_ORD_BIG')
ORDER  BY table_name, constraint_type;
-- constraint_type 에 'R'(FK)이 한 건도 없으면 모든 관계가 논리적임을 확인.

-- (4) 인덱스 목록 — T_ORD_BIG 는 0건이어야 함
SELECT table_name, index_name, uniqueness
FROM   user_indexes
WHERE  table_name IN ('M_RGN','M_ITM','M_ITM_PRC_HIS','M_CUS',
                      'T_ORD','T_ORD_DET','T_ITM_EVL','T_ORD_BIG')
ORDER  BY table_name, index_name;

-- (5) 인덱스 컬럼 구성(복합키 순서 확인)
SELECT index_name, column_position, column_name
FROM   user_ind_columns
WHERE  table_name IN ('M_ITM_PRC_HIS','T_ORD_DET','T_ITM_EVL')
ORDER  BY index_name, column_position;

-- (6) 인덱스 클러스터링 팩터(CF) 확인 — 인덱스를 만든 뒤 비교
SELECT index_name, leaf_blocks, distinct_keys, clustering_factor, num_rows
FROM   user_indexes
WHERE  table_name = 'T_ORD_BIG';

-- (7) 데이터 편향 직접 확인 (히스토그램 판단 근거)
SELECT cus_gd, COUNT(*) FROM M_CUS GROUP BY cus_gd ORDER BY cus_gd;   -- A/B/C
SELECT ord_st, COUNT(*) FROM T_ORD GROUP BY ord_st ORDER BY ord_st;  -- COMPLETE/PENDING/CANCEL

-- 참고: 파티션 테이블은 없으므로 user_part_tables 는 0건(확인용)
SELECT table_name, partitioning_type FROM user_part_tables;