Skip to content

스키마 A — SCOTT + 주문(기본 실습) · ERD · 조감도 · 인덱스 설명서 ​

실행 계정: sqlp · 만드는 방법: Oracle_실습환경_구축가이드.md의 Quick Start를 따라 접속한 뒤 00_sample_schema_setup.sql을 통째로 실행한다(수십 초에서 1~2분).


1. ERD ​

mermaid
erDiagram
    DEPT ||--o{ EMP : "1:N (deptno)"
    EMP  ||--o{ EMP : "1:N (mgr→empno 자기참조)"
    CUSTOMERS ||--o{ ORDERS : "1:N (cust_id) · 논리FK"
    PRODUCTS  ||--o{ ORDERS : "1:N (prod_id) · 논리FK"

    DEPT {
        NUMBER deptno PK
        VARCHAR2 dname
        VARCHAR2 loc
    }
    EMP {
        NUMBER empno PK
        VARCHAR2 ename
        VARCHAR2 job
        NUMBER mgr
        DATE hiredate
        NUMBER sal
        NUMBER comm
        NUMBER deptno FK
    }
    SALGRADE {
        NUMBER grade
        NUMBER losal
        NUMBER hisal
    }
    PRODUCTS {
        NUMBER prod_id PK
        VARCHAR2 prod_name
        VARCHAR2 category
        NUMBER list_price
    }
    CUSTOMERS {
        NUMBER cust_id PK
        VARCHAR2 cust_name
        VARCHAR2 grade
        VARCHAR2 city
        DATE reg_date
    }
    ORDERS {
        NUMBER order_id PK
        NUMBER cust_id
        NUMBER prod_id
        DATE order_date
        VARCHAR2 status
        NUMBER qty
        NUMBER amount
    }

관계 메모

  • DEPT 1:N EMP: EMP.deptno → DEPT.deptno. 물리 FK가 걸려 있다(fk_emp_dept).
  • EMP 1:N EMP: EMP.mgr → EMP.empno. 자기참조(계층) 관계다. FK 제약은 없지만 논리적으로 계층을 이룬다(KING은 mgr이 NULL인 최상위). CONNECT BY와 재귀 WITH 실습에 쓴다.
  • CUSTOMERS 1:N ORDERS: ORDERS.cust_id → CUSTOMERS.cust_id. 논리적 관계다(FK는 걸지 않았다).
  • PRODUCTS 1:N ORDERS: ORDERS.prod_id → PRODUCTS.prod_id. 논리적 관계다(FK는 걸지 않았다).
  • SALGRADE: 어떤 테이블과도 키 관계가 없는 독립 테이블이다. EMP.sal BETWEEN SALGRADE.losal AND SALGRADE.hisal 형태로 비등가(범위) 조인하는 대상이라 ERD에는 선을 긋지 않았다.

텍스트(ASCII) 조감도 ​

                        [ 개념 학습용 (SCOTT) — 소형 ]
   +-----------+   FK(deptno)    +-----------------------+
   |   DEPT    |1 ------------- N|         EMP           |---+ mgr→empno
   | (4행)     |                 | (14행, 자기참조 계층) |<--+ (계층/자기조인)
   | PK deptno |                 | PK empno              |
   +-----------+                 | sal ──┐               |
                                 +-------│---------------+
                                         │ BETWEEN losal..hisal (범위조인)
                                 +-------▼--------+
                                 |   SALGRADE     |  (5행, 독립·비키)
                                 | grade/lo/hisal |
                                 +----------------+

                        [ 튜닝 실습용 (주문) — 대형 ]
   +----------------+                                   +----------------+
   |   CUSTOMERS    |1                                 1|    PRODUCTS    |
   | (20,000행)     |                                   | (2,000행)      |
   | PK cust_id     |                                   | PK prod_id     |
   | grade A1/B9/C90|                                   | category 5종   |
   | city 6종       |                                   +-------┬--------+
   +-------┬--------+                                           │
           │ N (cust_id, 논리FK)            (prod_id, 논리FK) N │
           │              +----------------------+             │
           +------------> |        ORDERS        | <-----------+
                          | (200,000행)          |
                          | PK order_id          |
                          | status COMP/PEND/CAN |
                          | order_date(~2년)     |
                          | amount/qty (무인덱스) |
                          +----------------------+

2. 테이블 조감도 ​

테이블의미PK대략 행수핵심 컬럼관계
DEPT부서(SCOTT 고전)pk_dept(deptno)4deptno, dname, locEMP의 부모(1:N, deptno)
EMP사원(SCOTT 고전 · 계층)pk_emp(empno)14empno, ename, job, mgr, hiredate, sal, comm, deptnoDEPT의 자식(FK deptno) · EMP 자기참조(mgr→empno) · sal로 SALGRADE와 범위 조인
SALGRADE급여 등급표(독립 · 키 없음)없음5grade, losal, hisal키 관계 없음. EMP.sal BETWEEN losal..hisal 비등가 조인
PRODUCTS상품pk_products(prod_id)2,000prod_id, prod_name, category(5종), list_priceORDERS의 부모(1:N, prod_id, 논리FK)
CUSTOMERS고객pk_customers(cust_id)20,000cust_id, cust_name, grade(A/B/C 편향), city(6종), reg_dateORDERS의 부모(1:N, cust_id, 논리FK)
ORDERS주문(대용량 팩트)pk_orders(order_id)200,000order_id, cust_id, prod_id, order_date(~2년), status(편향), qty, amountCUSTOMERS · PRODUCTS의 자식(논리FK)

데이터 분포(편향): 히스토그램 실습의 핵심

컬럼분포생성 규칙
CUSTOMERS.gradeA ≈ 1% / B ≈ 9% / C ≈ 90%MOD(level,100)=0→A, MOD(level,10)=0→B, else C
CUSTOMERS.city6종 균등(각 ≈ 1/6)MOD(level,6) → SEOUL/BUSAN/INCHEON/DAEGU/DAEJEON/GWANGJU
CUSTOMERS.reg_date약 1000일 균등DATE '2022-01-01' + MOD(level,1000)
PRODUCTS.category5종 균등(각 ≈ 1/5)MOD(level,5) → ELECTRONICS/BOOK/FASHION/FOOD/TOY
ORDERS.statusCOMPLETE ≈ 90% / PENDING ≈ 8% / CANCEL ≈ 2%MOD(level,50)=0→CANCEL, MOD(level,10)=0→PENDING, else COMPLETE
ORDERS.order_date약 2년(730일) 균등DATE '2024-01-01' + MOD(level,730)
ORDERS.cust_id / prod_id1~20000 / 1~2000 순환 균등MOD(level,20000)+1 / MOD(level,2000)+1

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

생성되는 인덱스 (PK 5개 + 보조 3개 = 총 8개) ​

DDL을 돌리면 실제로 생기는 인덱스는 다음과 같다. PK 제약 5개가 고유 인덱스를 하나씩 자동으로 만들고, 보조 인덱스 3개는 DDL에 명시해서 만든다.

인덱스대상종류용도/실습 포인트
pk_deptDEPT(deptno)PK(고유)기본
pk_empEMP(empno)PK(고유)자기참조 계층 조인의 inner
pk_productsPRODUCTS(prod_id)PK(고유)ORDERS→PRODUCTS NL 조인에서 inner 쪽 진입
pk_customersCUSTOMERS(cust_id)PK(고유)ORDERS→CUSTOMERS NL 조인에서 inner 쪽 진입
pk_ordersORDERS(order_id)PK(고유)단건 조회
orders_x_custORDERS(cust_id)보조(비고유)특정 고객의 주문을 조회할 때 인덱스 vs Full Scan
orders_x_dateORDERS(order_date)보조(비고유)기간 범위 검색, 클러스터링 팩터(CF) 관찰
cust_x_cityCUSTOMERS(city)보조(비고유)균등 분포 컬럼의 인덱스 선택도 실습

SALGRADE에는 일부러 PK도 인덱스도 두지 않았다. grade NUMBER, losal, hisal 모두 제약이 없다.

일부러 인덱스를 만들지 않은 컬럼 (DDL 주석에 명시) ​

  • ORDERS.status(편향 90/8/2): 편향된 컬럼에 히스토그램을 더해 옵티마이저의 카디널리티 추정을 실습한다.
  • ORDERS.amount: 인덱스 없이 범위 조회와 집계를 해서, Full Scan과 인덱스를 비교하는 기준선으로 쓴다.
  • ORDERS.prod_id, ORDERS.qty: 인덱스가 없다. prod_id는 논리FK라서 조인할 때 인덱스가 없으면 어떤 영향이 있는지 관찰한다.
  • CUSTOMERS.grade(편향 1/9/90): 히스토그램(method_opt=>'FOR ALL COLUMNS SIZE AUTO'로 수집)의 효과를 실습한다.

무엇을 실습하나 ​

  1. 데이터 분포 편향 + 히스토그램: CUSTOMERS.grade, ORDERS.status. AUTO 히스토그램이 생기면 grade='A'(1%)와 'C'(90%)의 실행계획이 달라지는지 관찰한다.
  2. 인덱스 vs Full Scan: 인덱스가 있는 cust_id/order_date/city와 인덱스가 없는 status/amount를 비교한다. 조건이 고르는 행의 비율(선택도)이 클수록 Full Scan이 유리하고, 작을수록 인덱스가 유리하다. 인덱스가 없는 컬럼은 Full Scan밖에 길이 없다.
  3. 클러스터링 팩터(CF): orders_x_date는 적재 순서(MOD(level,730))와 물리 순서가 어긋나서 CF가 나쁘다. 이때 인덱스 범위 검색의 효율이 어떤지 관찰한다.
  4. 조인 방식(NL vs Hash): ORDERS(20만) ⨝ CUSTOMERS(2만)/PRODUCTS(2천). PK 인덱스가 있는지, 결과 집합이 얼마나 큰지에 따라 옵티마이저가 NL과 Hash 중 무엇을 고르는지 본다.
  5. 비등가(범위) 조인: EMP.sal BETWEEN SALGRADE.losal AND SALGRADE.hisal.
  6. 계층/자기조인: EMP.mgr → EMP.empno(CONNECT BY PRIOR, 재귀 WITH).
  7. 결합 인덱스 직접 설계: 가이드에서는 4주차에 (cust_id, order_date) 같은 결합 인덱스를 만들어 단일 인덱스와 비교한다(지금은 만들어 두지 않았다).

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

sql
-- 접속: sqlp/sqlp

-- (1) 테이블 행수 (통계 기준 + 실측)
SELECT table_name, num_rows, blocks, last_analyzed
FROM   user_tables
WHERE  table_name IN ('DEPT','EMP','SALGRADE','PRODUCTS','CUSTOMERS','ORDERS')
ORDER  BY num_rows;

-- 실측 행수
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
ORDER BY t;

-- (2) 인덱스 목록 + 구성 컬럼 + 클러스터링 팩터
SELECT i.table_name, i.index_name, i.uniqueness,
       i.clustering_factor, i.num_rows,
       ic.column_position, ic.column_name
FROM   user_indexes i
JOIN   user_ind_columns ic
       ON ic.index_name = i.index_name
WHERE  i.table_name IN ('DEPT','EMP','SALGRADE','PRODUCTS','CUSTOMERS','ORDERS')
ORDER  BY i.table_name, i.index_name, ic.column_position;

-- (3) 제약(PK/FK) 확인 — 물리 FK는 EMP.deptno 하나뿐
SELECT c.table_name, c.constraint_name, c.constraint_type,  -- P=PK, R=FK
       cc.column_name, c.r_constraint_name
FROM   user_constraints c
JOIN   user_cons_columns cc ON cc.constraint_name = c.constraint_name
WHERE  c.table_name IN ('DEPT','EMP','SALGRADE','PRODUCTS','CUSTOMERS','ORDERS')
  AND  c.constraint_type IN ('P','R')
ORDER  BY c.table_name, c.constraint_type;

-- (4) 컬럼 통계 + 히스토그램 유무 (편향 실습)
SELECT table_name, column_name, num_distinct, num_nulls, histogram
FROM   user_tab_col_statistics
WHERE  table_name IN ('CUSTOMERS','ORDERS')
  AND  column_name IN ('GRADE','CITY','STATUS','AMOUNT','PROD_ID','CUST_ID','ORDER_DATE')
ORDER  BY table_name, column_name;

-- (5) 편향 분포 직접 확인
SELECT grade, COUNT(*) FROM customers GROUP BY grade ORDER BY grade;    -- A/B/C ≈ 1/9/90
SELECT status, COUNT(*) FROM orders   GROUP BY status ORDER BY status;  -- COMPLETE/PENDING/CANCEL

이 스키마에는 파티션 테이블이 없으므로 user_part_tables를 조회해도 빈 결과가 나온다.