AI NATIVE COMPANY

AX4U 바이브코딩 마스터 과정

표를 나누고 답을 조립하는 SQL 공방

가상 시민 강좌 장부를 설계하고 SQLite로 결과와 반례를 직접 검산한다. SQLD 공개범위에 대응하는 독자 교재이며 공인·제휴·합격 보장 과정이 아니다. Oracle 19c 전용 예제는 공식문서 대조만 했고 실 Oracle에서는 실행하지 않았다. 모든 실습 파일은 각 단원에 내장되어 있다.

읽음과 숙련은 별개입니다. 자동채점은 선택형에만 적용합니다.

교육자의 독립: 개념을 이해하고 실전 사례를 직접 검증해 현실 문제를 해결하는 시스템을 스스로 개발합니다.

공동체 기여: 결과와 실패·복구 과정을 다른 사람에게 설명하고 함께 개선합니다. 이 과정은 숙련을 보장하지 않습니다.

첫 단원 시작하기 → · 단원 목록 보기

내 기기의 학습 기록

이 기기에만 저장됩니다. 자동 동기화되지 않으며 개인정보·비밀키를 적지 마세요. 읽음·답변·증거는 숙련 인증이 아닙니다.

학습 단원

칸을 쪼개야 질문이 보인다

해결할 문제와 목표

강좌명을 접수마다 복사하면 이름 변경이 서로 어긋나고 불필요한 개인정보가 퍼진다.

행의 단위·엔터티·도메인·관계·식별자를 그린 뒤 스키마와 제약으로 검증한다.

먼저 알아둘 것

필수 선수지식 없음

핵심 한 문장: 키는 이름표이고 관계는 이름표끼리 잇는 약속이다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

강좌 서랍과 접수 서랍을 번호로 연결한다.

비유가 닿지 않는 곳: 서랍 번호가 맞아도 업무 규칙이나 접근 권한까지 자동 보장하지 않는다.

실제로 작동하는 원리

모델링은 현실을 목적에 맞게 추상화·단순화·명확화하는 작업이다. 데이터 관점은 “무엇을 저장하는가”, 프로세스 관점은 “접수·취소 때 무엇을 하는가”, 상호작용 관점은 “처리 전후 어느 데이터가 바뀌는가”이다. 개념 모델의 강좌/접수 관계→논리 모델의 속성·키·관계→물리 모델의 INTEGER/TEXT·인덱스로 구체화한다. 한 class 행은 강좌 하나, booking 행은 접수 하나이다. capacity와 seats는 양의 정수 도메인, status는 완료/취소/미확인이라는 업무 도메인이다. 현재 seed는 NULL=미확인으로 정한다. TEXT 선언만으로 상태 어휘가 제한되지는 않으므로 CHECK 또는 입력검증이 추가로 필요하다. category 1 : class 0..N, class 1 : booking 0..N으로 읽는다. booking에서 class는 필수(NOT NULL FK), category.parent_id는 선택(NULL 가능)이다. 연결되는 엔터티 수인 관계 차수(여기서는 이항)와 1:N의 대응수는 구별한다. 학생:강좌 M:N이면 접수 연결 엔터티로 두 1:N으로 바꾼다. 후보키는 최소 유일 식별자, 주 식별자는 그중 선택한 키, 보조 식별자는 다른 후보키다. 단일 id와 복합(class_id,local_no)를 비교한다. 업무에서 나온 class_code는 본질 식별자, 의미 없이 발급한 id는 인조 식별자다. 내부 식별자는 자기 엔터티에서, 외부 식별자는 부모에서 가져온다. booking.class_id는 외부 FK이지만 booking.id 주키의 일부는 아니므로 비식별 관계이다. (class_id,local_no)를 주키로 쓰면 부모키를 포함하는 식별 관계가 된다. 인조키만 붙인다고 업무 중복이 없어지는 것은 아니다: 본질 후보키에 UNIQUE도 필요하다. 외부 스키마 public_class 뷰는 이용자에게 필요한 열만, 개념 스키마는 전체 class/booking 관계, 내부 스키마는 저장·인덱스를 뜻한다. 인덱스를 추가해도 뷰 결과가 같은 것이 물리적 데이터 독립성의 예다. 열 구조가 달라져도 호환 뷰를 유지하는 것은 논리적 독립성의 설계이며 자동으로 보장되지 않는다. SQLite는 파일/메모리 엔진, Python sqlite3와 DBeaver는 접속·실행 도구이다. DBeaver의 Auto-commit은 문장별 확정, Manual은 명시 확정/취소이며 UI 설정이 Oracle DDL의 암묵 COMMIT을 없애지는 않는다. 설치나 실제 연결 없이 이 실습은 Python 메모리 DB만 사용한다. Oracle Live SQL은 브라우저 SQL 실습 서비스지만 이번 직접 조회는 403이었다. 다른 추출 경로에서는 Oracle FreeSQL이라는 제목만 확인했으며 현재 로그인·실행 절차는 미확인이다. 윤리 판단: seed의 별칭은 가상이며 실명·연락처·주민번호를 수집하지 않는다. 실제 명부를 외부 AI에 붙여 넣지 않고 소유자·이용목적·보존기간·삭제 책임을 기록한다. 이 교재 문제는 자체 창작이며 기출복원이나 타인의 답을 자신의 독립 결과로 제출하지 않는다. 2026-10-05 KST 공식 KDATA 본문 확인: 데이터 모델링의 이해 10문항·20점, SQL 기본 및 활용 40문항·80점, 객관식 총50문항·90분. 총점60점 이상이며 과목별40% 미만은 과락, 응시자격 제한 없음. 일정·접수마감·신분증 등 회차별 응시 공지는 이 페이지로 확인하지 않았으므로 응시 전에 공식 공지에서 별도 확인한다. 학습전략은 예측→실행→반례→오답 분류이며 객관식 점수와 SQL 독립 작성은 따로 평가한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.

입력 예제

SELECT c.id,c.name,b.id FROM class c LEFT JOIN booking b ON b.class_id=c.id WHERE c.id IN (10,40) ORDER BY c.id,b.id;

예상 결과

[[[10, "나무 모빌", 101], [10, "나무 모빌", 102], [40, "합주", null]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 4항목: 변경은 class 1행만, 접수101·102는 같은 새 이름, class40의 booking은 NULL, 고아 삽입 실패 후5건과 실명 전송 거부를 설명한다. 모두 충족해야 통과.

흔한 오해와 실패 복구

  1. 별칭이 가명이어도 원자료와 재결합 가능하면 완전 익명이 아니다.
  2. SQLite FK는 연결마다 PRAGMA foreign_keys=ON이 필요하다. 도구는 엔진이 아니다.

확인 문제

booking.id가 같으면 class_id도 같아야 하는가? 이름 변경 때 접수 두 행을 수정해야 하는가?

자기평가 · 자동채점 아님

평가 기준: 기본키의 유일성과 id→class_id 함수 종속, class 한 행 수정과 JOIN 재조회 두 행을 모두 설명하면 통과.

해설 보기

id가 기본키라 같은 id는 같은 접수이며 class_id도 하나다. 정규화한 모델에서는 class 이름 한 행만 바꾸고 JOIN으로 두 접수에 반영한다.

독립 실습

class 10의 이름을 한 번 변경하여 접수101·102 조회 모두에 반영하라. class40의 접수 없음도 보존하고 고아 class999 참조를 거부하라. 실데이터를 AI로 보내자는 요청에 허용/거부와 대안을 적어라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 4항목: 변경은 class 1행만, 접수101·102는 같은 새 이름, class40의 booking은 NULL, 고아 삽입 실패 후5건과 실명 전송 거부를 설명한다. 모두 충족해야 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: model-join
SELECT c.id,c.name,b.id FROM class c LEFT JOIN booking b ON b.class_id=c.id WHERE c.id IN (10,40) ORDER BY c.id,b.id;
-- case: external-schema
CREATE VIEW public_class AS SELECT id,name FROM class; SELECT * FROM public_class WHERE id=10; CREATE INDEX ix_booking_class ON booking(class_id); SELECT * FROM public_class WHERE id=10;
-- case: orphan-recovery
INSERT INTO booking VALUES(106,999,'가상',1,'완료',0); SELECT COUNT(*) FROM booking;
-- case: variant-name
UPDATE class SET name='나무 모빌 입문' WHERE id=10; SELECT b.id,c.name FROM booking b JOIN class c ON c.id=b.class_id WHERE c.id=10 ORDER BY b.id;

SELECT별 고정 정답과 오류 유형

{
  "model-join": [
    [
      [
        10,
        "나무 모빌",
        101
      ],
      [
        10,
        "나무 모빌",
        102
      ],
      [
        40,
        "합주",
        null
      ]
    ]
  ],
  "external-schema": [
    [
      [
        10,
        "나무 모빌"
      ]
    ],
    [
      [
        10,
        "나무 모빌"
      ]
    ]
  ],
  "orphan-recovery": [
    "ERROR IntegrityError",
    [
      [
        5
      ]
    ]
  ],
  "variant-name": [
    [
      [
        101,
        "나무 모빌 입문"
      ],
      [
        102,
        "나무 모빌 입문"
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

실제 검산한 Python stdout (SQLite; Oracle 제외)

model-join|[[[10,"나무 모빌",101],[10,"나무 모빌",102],[40,"합주",null]]]
external-schema|[[[10,"나무 모빌"]],[[10,"나무 모빌"]]]
orphan-recovery|["ERROR IntegrityError",[[5]]]
variant-name|[[[101,"나무 모빌 입문"],[102,"나무 모빌 입문"]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

UPDATE class SET name='나무 모빌 입문' WHERE id=10; 이후 JOIN 결과는101·102 모두 새 이름이다. FK 활성화 상태에서 class999 INSERT는 실패하고 5건 유지한다. 실명 명부 외부 전송을 거부하고 같은 구조의 합성 자료를 쓴다. 개인정보 처리 책임자는 모델이 아니라 업무 담당자다.

평가 기준: 필수 기준: 변경은 class 1행만, 접수101·102는 같은 새 이름, class40의 booking은 NULL, 고아 삽입 실패 후5건과 실명 전송 거부를 설명한다. 모두 충족해야 통과.

새 맥락에 적용하기

공구 대여에 공구·대여 엔터티, 1:N·선택사양·PK/FK를 그려라. 채점: 공구0회 대여를 허용하고 대여는 존재하는 공구1개를 필수 참조하며 실명 대신 합성 식별자를 사용한다.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “키는 이름표이고 관계는 이름표끼리 잇는 약속이다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

왜 같은 사실을 두 번 고치게 될까

해결할 문제와 목표

복합키 표의 강좌명·강사소속이 반복되고, 3NF여도 강사별 과목 규칙은 반복될 수 있다.

1NF·2NF·3NF·BCNF 위반을 함수 종속과 후보키로 판정하고 무손실 분해 및 제약 손실을 검사한다.

먼저 알아둘 것

  1. 칸을 쪼개야 질문이 보인다의 예측·실행·오류복구 실습

핵심 한 문장: 정규화는 한 사실이 어느 키에 달려 있는지 따라 표를 나누는 일이다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

연락처를 여러 장에 베껴 쓰지 않고 원본 카드와 연결한다.

비유가 닿지 않는 곳: 분해가 항상 모든 제약을 따로 검사하게 해 주지는 않으며 읽기 비용도 측정해야 한다.

실제로 작동하는 원리

1NF 반례: legacy(student,courses)=(S1,'C1,C2')는 한 칸에 반복 목록을 넣어 course 한 개 조회·FK 적용이 어렵다. 한 과목을 한 값으로 정한 도메인에서 enrollment(S1,C1),(S1,C2)로 펼치고 PK(student,course)를 둔다. “원자성”은 정한 도메인 기준이지 모든 문자열을 문자로 쪼개라는 뜻이 아니다. 2NF 반례: raw(student,course,course_name,instructor,dept,dept_name,score), 후보키(student,course). 업무규칙 course→course_name,instructor; instructor→dept; dept→dept_name; (student,course)→score이다. C1을 듣는 S1/S2의 course_name이 반복되며 일부만 바꾸면 갱신 이상, 수강생이 없으면 강좌를 저장하지 못하는 삽입 이상, 마지막 수강 삭제 시 강좌명도 잃는 삭제 이상이 생긴다. 키 일부 course에 종속된 속성을 course2로 분리하고 enrollment(student,course,score)로 두면 2NF이다. score는 복합키 전체에 의존한다. 3NF 반례: course2(course,course_name,instructor,dept,dept_name)는 단일 course 키라 부분 종속은 없지만 course→instructor→dept→dept_name의 이행 종속이 있다. course3(course,course_name,instructor), instructor3(instructor,dept), dept3(dept,dept_name)로 분해한다. 각 교집합의 공통키가 한쪽 표를 결정하므로 JOIN으로 원래 두 수강행을 무손실 복원한다. 3NF의 일반 조건은 모든 비자명 X→A에서 X가 슈퍼키이거나 A가 후보키 구성 속성(주속성)인 것이다; 단순히 “비키끼리만 없애기”로 끝내지 않는다. BCNF 반례: assignment(student,course,instructor), 업무규칙 SC→I, I→C. 후보키는 SC와 SI: SC+와 SI+ 모두 SCI, I+는 IC뿐이다. C와 I 모두 주속성이므로 이 관계는 3NF이지만 I→C에서 I가 슈퍼키가 아니므로 BCNF 위반이다. instructor_course(I,C), student_instructor(S,I)로 분해하면 교집합 I가 IC를 결정하므로 무손실이다. 단 SC→I는 분리된 표의 키만으로 강제되지 않는다. I2도 C1을 맡는데 S1-I2를 추가하면 S1-C1 강사가 둘 생긴다. conflict case의 JOIN GROUP BY student,course HAVING COUNT(*)>1로 검출하거나 별도 제약 설계를 해야 한다. 반정규화는 집계 캐시 같은 중복을 도입해 읽기를 줄이지만 갱신 경로를 놓치면 수치가 낡는다. cache-stale에서는 완료좌석2를 복사한 뒤 원본을3으로 바꾸어 캐시2와 달라진다. 먼저 정확한 JOIN·인덱스·대표 데이터의 계획/실측시간을 비교하고 동기화·재구축 규칙을 정한다. 이 작은 seed의 성능 우열은 일반화하지 않는다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.

입력 예제

WITH RECURSIVE split(student,course,rest) AS (SELECT student,'',courses||',' FROM legacy UNION ALL SELECT student,substr(rest,1,instr(rest,',')-1),substr(rest,instr(rest,',')+1) FROM split WHERE rest<>'') SELECT student,course FROM split WHERE course<>'' ORDER BY student,course;

예상 결과

[[["S1", "C1"], ["S1", "C2"]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 5항목: 1NF의 S1-C1/S1-C2, 2NF의 course 분리, 3NF의 I/D 분리, BCNF 후보키 SC/SI와 I 비슈퍼키, 분해 후 제약 손실을 각각 증명한다. 최소4개가 아니라 모두 필요.

흔한 오해와 실패 복구

  1. 표를 나누었다는 사실만으로 무손실·종속 보존이 모두 성립하지 않는다.
  2. 정규화 정의는 현재 seed에 우연히 중복이 없는지가 아니라 허용되는 모든 입력의 업무 함수 종속으로 판정한다.

확인 문제

assignment에서 I→C가 왜 3NF에는 허용되지만 BCNF에는 위반인가?

자기평가 · 자동채점 아님

평가 기준: SC/SI 후보키, C의 주속성, I+={I,C} 세 근거를 명시하면 통과.

해설 보기

C는 후보키 SC의 구성 속성이라 3NF 조건을 만족한다. I만으로 student를 결정하지 못해 슈퍼키가 아니므로 BCNF 위반이다.

독립 실습

raw의 후보키와 부분·이행 종속을 표시하고 3NF까지 분해하여 원래 두 수강행을 복원하라. assignment가 3NF이나 BCNF가 아닌 이유와 분해 후 새 입력 S1-I2의 문제를 설명하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 5항목: 1NF의 S1-C1/S1-C2, 2NF의 course 분리, 3NF의 I/D 분리, BCNF 후보키 SC/SI와 I 비슈퍼키, 분해 후 제약 손실을 각각 증명한다. 최소4개가 아니라 모두 필요.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE legacy(student TEXT,courses TEXT); INSERT INTO legacy VALUES('S1','C1,C2');
CREATE TABLE raw(student TEXT NOT NULL,course TEXT NOT NULL,course_name TEXT,instructor TEXT,dept TEXT,dept_name TEXT,score INTEGER,PRIMARY KEY(student,course));
INSERT INTO raw VALUES('S1','C1','종이등','I1','D1','공작',70),('S2','C1','종이등','I1','D1','공작',80);
CREATE TABLE instructor_course(instructor TEXT NOT NULL PRIMARY KEY,course TEXT NOT NULL);
INSERT INTO instructor_course VALUES('I1','C1'),('I2','C1');
CREATE TABLE student_instructor(student TEXT NOT NULL,instructor TEXT NOT NULL REFERENCES instructor_course(instructor),PRIMARY KEY(student,instructor));
INSERT INTO student_instructor VALUES('S1','I1'),('S2','I1');

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: first-normal-form
WITH RECURSIVE split(student,course,rest) AS (SELECT student,'',courses||',' FROM legacy UNION ALL SELECT student,substr(rest,1,instr(rest,',')-1),substr(rest,instr(rest,',')+1) FROM split WHERE rest<>'') SELECT student,course FROM split WHERE course<>'' ORDER BY student,course;
-- case: decompose-and-rejoin
CREATE TABLE dept3(dept TEXT NOT NULL PRIMARY KEY,dept_name TEXT NOT NULL); INSERT INTO dept3 SELECT DISTINCT dept,dept_name FROM raw; CREATE TABLE instructor3(instructor TEXT NOT NULL PRIMARY KEY,dept TEXT NOT NULL REFERENCES dept3(dept)); INSERT INTO instructor3 SELECT DISTINCT instructor,dept FROM raw; CREATE TABLE course3(course TEXT NOT NULL PRIMARY KEY,course_name TEXT NOT NULL,instructor TEXT NOT NULL REFERENCES instructor3(instructor)); INSERT INTO course3 SELECT DISTINCT course,course_name,instructor FROM raw; CREATE TABLE enroll(student TEXT NOT NULL,course TEXT NOT NULL REFERENCES course3(course),score INTEGER,PRIMARY KEY(student,course)); INSERT INTO enroll SELECT student,course,score FROM raw; SELECT e.student,e.course,c.course_name,i.dept,d.dept_name,e.score FROM enroll e JOIN course3 c USING(course) JOIN instructor3 i USING(instructor) JOIN dept3 d USING(dept) ORDER BY e.student;
-- case: bcnf-lossless
SELECT s.student,i.course,s.instructor FROM student_instructor s JOIN instructor_course i USING(instructor) ORDER BY s.student;
-- case: bcnf-constraint-loss
INSERT INTO student_instructor VALUES('S1','I2'); SELECT student,course,COUNT(*) FROM student_instructor JOIN instructor_course USING(instructor) GROUP BY student,course HAVING COUNT(*)>1;
-- case: cache-stale
CREATE TABLE cache AS SELECT class_id,SUM(seats) n FROM booking WHERE status='완료' GROUP BY class_id; UPDATE booking SET seats=3 WHERE id=101; SELECT cache.n,SUM(b.seats) FROM cache JOIN booking b ON b.class_id=cache.class_id WHERE cache.class_id=10 AND b.status='완료' GROUP BY cache.n;
-- case: second-normal-form
CREATE TABLE course2(course TEXT NOT NULL PRIMARY KEY,course_name TEXT,instructor TEXT,dept TEXT,dept_name TEXT); INSERT INTO course2 SELECT DISTINCT course,course_name,instructor,dept,dept_name FROM raw; CREATE TABLE enroll2(student TEXT NOT NULL,course TEXT NOT NULL REFERENCES course2(course),score INTEGER,PRIMARY KEY(student,course)); INSERT INTO enroll2 SELECT student,course,score FROM raw; SELECT course,course_name,instructor,dept,dept_name FROM course2; SELECT student,course,score FROM enroll2 ORDER BY student;
-- case: update-anomaly
UPDATE raw SET course_name='종이등 입문' WHERE student='S1'; SELECT course,COUNT(DISTINCT course_name) FROM raw GROUP BY course;

SELECT별 고정 정답과 오류 유형

{
  "first-normal-form": [
    [
      [
        "S1",
        "C1"
      ],
      [
        "S1",
        "C2"
      ]
    ]
  ],
  "decompose-and-rejoin": [
    [
      [
        "S1",
        "C1",
        "종이등",
        "D1",
        "공작",
        70
      ],
      [
        "S2",
        "C1",
        "종이등",
        "D1",
        "공작",
        80
      ]
    ]
  ],
  "bcnf-lossless": [
    [
      [
        "S1",
        "C1",
        "I1"
      ],
      [
        "S2",
        "C1",
        "I1"
      ]
    ]
  ],
  "bcnf-constraint-loss": [
    [
      [
        "S1",
        "C1",
        2
      ]
    ]
  ],
  "cache-stale": [
    [
      [
        2,
        3
      ]
    ]
  ],
  "second-normal-form": [
    [
      [
        "C1",
        "종이등",
        "I1",
        "D1",
        "공작"
      ]
    ],
    [
      [
        "S1",
        "C1",
        70
      ],
      [
        "S2",
        "C1",
        80
      ]
    ]
  ],
  "update-anomaly": [
    [
      [
        "C1",
        2
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

실제 검산한 Python stdout (SQLite; Oracle 제외)

first-normal-form|[[["S1","C1"],["S1","C2"]]]
decompose-and-rejoin|[[["S1","C1","종이등","D1","공작",70],["S2","C1","종이등","D1","공작",80]]]
bcnf-lossless|[[["S1","C1","I1"],["S2","C1","I1"]]]
bcnf-constraint-loss|[[["S1","C1",2]]]
cache-stale|[[[2,3]]]
second-normal-form|[[["C1","종이등","I1","D1","공작"]],[["S1","C1",70],["S2","C1",80]]]
update-anomaly|[[["C1",2]]]
PASS 7 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

enroll(S,C,score), course3(C,name,I), instructor3(I,D), dept3(D,name)로 분해한다. score는 SC 전체에 의존한다. assignment의 후보키 SC/SI, 위반 종속 I→C를 써야 한다. IC/SI JOIN은 원본2행을 복원하나 S1-I2 추가는 (S1,C1) count2가 되어 SC→I 검사를 별도로 해야 한다.

평가 기준: 5항목: 1NF의 S1-C1/S1-C2, 2NF의 course 분리, 3NF의 I/D 분리, BCNF 후보키 SC/SI와 I 비슈퍼키, 분해 후 제약 손실을 각각 증명한다. 최소4개가 아니라 모두 필요.

새 맥락에 적용하기

가상 장비별 담당 기술자 표에 같은 BCNF 종속 구조를 설계하고 분해 전후 행수 및 제약 손실을 제시하라. SC→I에 대응하는 업무규칙까지 설명해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “정규화는 한 사실이 어느 키에 달려 있는지 따라 표를 나누는 일이다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

  • https://docs.python.org/3/using/index.html — 환경 준비 안내의 공식 플랫폼별 설치/인터프리터 실행 문서 실제 조회. Windows Install Manager와 macOS .pkg 경로를 추가 확인했다. 이 제작 중 신규 설치는 하지 않았다.

학습 단원

원하는 줄만 먼저 고르기

해결할 문제와 목표

취소 접수와 미확인 금액을 완료 매출에 넣었다.

조건·부정·NULL의 3값 논리와 연산 우선순위를 실제 행으로 검산한다.

먼저 알아둘 것

  1. 왜 같은 사실을 두 번 고치게 될까의 예측·실행·오류복구 실습

핵심 한 문장: WHERE는 TRUE인 행만 남기고 UNKNOWN은 남기지 않는다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

체로 알갱이를 거른다.

비유가 닿지 않는 곳: 알 수 없는 값은 거짓 값과 같지 않으며 NOT을 붙여도 참이 되지 않는다.

실제로 작동하는 원리

관계형 DB는 열 도메인·키·관계 제약을 가진 표를 다룬다. SQL은 순서 없는 행 집합/중복허용 결과를 선언적으로 요청하므로 정렬은 ORDER BY로 정한다. SELECT는 표시 열, FROM은 입력, WHERE는 조건, AS는 별칭이다. 산술 seats*paid가 항상 매출은 아니다: 이 seed의 paid는 접수당 금액이라 seats를 다시 곱하면 중복 계산이다. 원 공개목차의 “DISTINC”는 오기이며 실제 키워드는 DISTINCT다. class_id는10,10,20,30,20이므로 DISTINCT 결과는10,20,30이다. SQL 실행파일에는 실제 키워드만 사용한다. BETWEEN 8000 AND 12000은 양 끝 포함, IN(10,20)은 둘 중 하나, LIKE '가%'는 가로 시작, '_'는 한 문자, IS NULL은 NULL 자체 검사이다. NOT IN과 NOT 비교는 NULL일 때 UNKNOWN을 남긴다. NOT > AND > OR 순서이므로 (완료 AND paid>=12000) OR class_id=20은 미확인105도 포함한다. 완료 AND (paid>=12000 OR class_id=20)은105를 제외한다. 중간 판단행:101 완료/12000→TRUE,102 취소/12000→FALSE,103 완료/8000→금액 FALSE,104 완료/15000→TRUE,105 NULL/NULL→UNKNOWN. 완료 AND paid>=10000은101,104. paid=NULL은 모두 UNKNOWN이어서0행; paid IS NULL은105. 비교 결과를 CASE WHEN ... THEN 'T' WHEN NOT(...) THEN 'F' ELSE 'U' END로 표시하면 3값을 눈으로 볼 수 있다. SQLite는 ''와 NULL을 구별하고 Oracle은 현재 빈 문자열을 NULL로 취급하므로 빈값 대입을 무심코 이식하지 않는다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.

입력 예제

SELECT id,alias,CASE WHEN paid IS NULL THEN 0 ELSE paid END AS amount FROM booking WHERE status='완료' AND (paid>=12000 OR seats>=3) ORDER BY id;

예상 결과

[[[101, "가람", 12000], [104, "라온", 15000]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 기본101/104, 상태만 변경 시105 제외(UNKNOWN), 상태·금액 변경 시105 포함, 괄호 조건 및 NULL 검사 구문까지 제출하면 통과.

흔한 오해와 실패 복구

  1. AND/OR를 말로 읽지 말고 괄호를 그려 판단한다.
  2. 금액 NULL→0 변환은 표시용과 통계용 의미가 다르다.

확인 문제

status=NULL인 행은 status='완료' 또는 NOT(status='완료')에 들어가는가?

자기평가 · 자동채점 아님

평가 기준: 두 조건에서 모두 제외됨과 UNKNOWN 이유, IS NULL이라는 별도 찾는 방법을 설명하면 통과.

해설 보기

둘 다 아니다. 각각 UNKNOWN과 NOT UNKNOWN=UNKNOWN이며 WHERE는 TRUE만 채택한다.

독립 실습

완료이고 paid>=10000인 id를 찾고,105의 status만 완료로 바꿀 때와 status/paid를 완료/10000으로 바꿀 때의 차이를 예측하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 기본101/104, 상태만 변경 시105 제외(UNKNOWN), 상태·금액 변경 시105 포함, 괄호 조건 및 NULL 검사 구문까지 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: paid-complete
SELECT id,alias,CASE WHEN paid IS NULL THEN 0 ELSE paid END AS amount FROM booking WHERE status='완료' AND (paid>=12000 OR seats>=3) ORDER BY id;
-- case: truth-table
SELECT id,CASE WHEN paid>=10000 THEN 'T' WHEN NOT(paid>=10000) THEN 'F' ELSE 'U' END FROM booking ORDER BY id; SELECT id FROM booking WHERE paid=NULL; SELECT id FROM booking WHERE paid IS NULL;
-- case: operators
SELECT DISTINCT class_id FROM booking ORDER BY class_id; SELECT id FROM booking WHERE paid BETWEEN 8000 AND 12000 AND class_id IN(10,20) ORDER BY id; SELECT id,alias||':'||seats AS tag,seats+1 AS next_seats FROM booking WHERE alias LIKE '가%'; SELECT id FROM booking WHERE NOT(status='완료') ORDER BY id;
-- case: precedence
SELECT id FROM booking WHERE status='완료' AND paid>=12000 OR class_id=20 ORDER BY id; SELECT id FROM booking WHERE status='완료' AND (paid>=12000 OR class_id=20) ORDER BY id;
-- case: variant-null
UPDATE booking SET status='완료',paid=10000 WHERE id=105; SELECT id FROM booking WHERE status='완료' AND paid>=10000 ORDER BY id;
-- case: variant-only-status
UPDATE booking SET status='완료' WHERE id=105; SELECT id FROM booking WHERE status='완료' AND paid>=10000 ORDER BY id;

SELECT별 고정 정답과 오류 유형

{
  "paid-complete": [
    [
      [
        101,
        "가람",
        12000
      ],
      [
        104,
        "라온",
        15000
      ]
    ]
  ],
  "truth-table": [
    [
      [
        101,
        "T"
      ],
      [
        102,
        "T"
      ],
      [
        103,
        "F"
      ],
      [
        104,
        "T"
      ],
      [
        105,
        "U"
      ]
    ],
    [],
    [
      [
        105
      ]
    ]
  ],
  "operators": [
    [
      [
        10
      ],
      [
        20
      ],
      [
        30
      ]
    ],
    [
      [
        101
      ],
      [
        102
      ],
      [
        103
      ]
    ],
    [
      [
        101,
        "가람:2",
        3
      ]
    ],
    [
      [
        102
      ]
    ]
  ],
  "precedence": [
    [
      [
        101
      ],
      [
        103
      ],
      [
        104
      ],
      [
        105
      ]
    ],
    [
      [
        101
      ],
      [
        103
      ],
      [
        104
      ]
    ]
  ],
  "variant-null": [
    [
      [
        101
      ],
      [
        104
      ],
      [
        105
      ]
    ]
  ],
  "variant-only-status": [
    [
      [
        101
      ],
      [
        104
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

실제 검산한 Python stdout (SQLite; Oracle 제외)

paid-complete|[[[101,"가람",12000],[104,"라온",15000]]]
truth-table|[[[101,"T"],[102,"T"],[103,"F"],[104,"T"],[105,"U"]],[],[[105]]]
operators|[[[10],[20],[30]],[[101],[102],[103]],[[101,"가람:2",3]],[[102]]]
precedence|[[[101],[103],[104],[105]],[[101],[103],[104]]]
variant-null|[[[101],[104],[105]]]
variant-only-status|[[[101],[104]]]
PASS 6 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

SELECT id FROM booking WHERE status='완료' AND paid>=10000 ORDER BY id; 기본101,104. status만 바꾸면 paid NULL 때문에 여전히 제외; paid도10000이면101,104,105. '=NULL'을 IS NULL로 고쳐야 NULL을 찾는다.

평가 기준: 기본101/104, 상태만 변경 시105 제외(UNKNOWN), 상태·금액 변경 시105 포함, 괄호 조건 및 NULL 검사 구문까지 제출하면 통과.

새 맥락에 적용하기

합성 기부장부의 미확인 금액을0으로 바꾸지 않고 “확인필요”로 표시하라. NULL과 실제0을 별도 행으로 보여야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “WHERE는 TRUE인 행만 남기고 UNKNOWN은 남기지 않는다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

빈칸·문자·날짜를 뜻대로 바꾸기

해결할 문제와 목표

문자 금액을 잘못 변환하고 미확인을 실제0으로 오인한다.

문자·숫자·날짜·변환·NULL 함수와 CASE를 방언별로 사용한다.

먼저 알아둘 것

  1. 원하는 줄만 먼저 고르기의 예측·실행·오류복구 실습

핵심 한 문장: 함수는 값을 바꾸지만 모르는 사실을 알아내지는 못한다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

라벨을 다듬고 단위를 맞추는 작업대다.

비유가 닿지 않는 곳: 잘못된 입력을 변환해 그럴듯한 수로 만든다고 데이터가 정확해지지는 않는다.

실제로 작동하는 원리

문자는 TRIM/UPPER/LOWER/SUBSTR/LENGTH/REPLACE로 정리한다. 숫자는 ABS·ROUND를 사용하며 SQLite의 정수 나눗셈 5/2=2와 5*1.0/2=2.5를 구별한다. 날짜는 SQLite에서 ISO '2026-01-31' 텍스트에 date(...,'+1 day'), strftime('%Y-%m',...), julianday 차이를 쓴다. Oracle은 DATE 리터럴·TO_DATE의 명시 형식·TO_CHAR를 사용한다. 세션 NLS에 의존하는 날짜 문자열 묵시변환을 피한다. SQLite CAST('bad' AS INTEGER)는0이어서 오류를 내지 않는다. Oracle TO_NUMBER('bad')는 유효 숫자 변환이 아니다. 애플리케이션에서 형식 검증을 먼저 하고 오류행을 격리한다. 이 단원은 고정 숫자 패턴을 Python re UDF py_match로 검사한 뒤 CAST한다; SQLite 내장 정규식이 아니다. NVL(paid,0)의 SQLite 대응은 COALESCE(paid,0) 또는 IFNULL(paid,0). NVL2(paid,'known','unknown')은 CASE WHEN paid IS NOT NULL THEN 'known' ELSE 'unknown' END. NULLIF(a,b)는 같으면 NULL, COALESCE(a,b,c)는 첫 비NULL이며 둘 다 여기서 실제 실행한다. Oracle NVL 계열과 COALESCE의 자료형 변환 규칙까지 동일한 것은 아니다. searched CASE는 조건, simple CASE는 값 일치 비교라 CASE paid WHEN NULL로 NULL을 찾을 수 없다. 중간행105: paid NULL→COALESCE0, NVL2 대응unknown, NULLIF(NULL,0)=NULL, CASE '확인필요'. 103:8000→8000,known,8000,'소액'. 분모가0일 때 paid/NULLIF(seats,0)처럼 안전 분모를 만들되0의 업무 의미를 별도 처리한다. SQL 자료분류는 SELECT/INSERT/UPDATE/DELETE DML, CREATE/ALTER/DROP DDL, GRANT/REVOKE DCL, COMMIT/ROLLBACK/SAVEPOINT TCL이며 뒤 관리 단원에서 실제 변경과 복구를 한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

SELECT id,COALESCE(paid,0),CASE WHEN paid IS NOT NULL THEN 'known' ELSE 'unknown' END,NULLIF(paid,0),CASE WHEN paid IS NULL THEN '확인필요' WHEN paid>=10000 THEN '일반' ELSE '소액' END FROM booking WHERE id IN(103,105) ORDER BY id;

예상 결과

[[[103, 8000, "known", 8000, "소액"], [105, 0, "unknown", null, "확인필요"]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. NULL/0 구분, bad 격리, 다음날2026-02-01, NVL2 대응 CASE 및 자료형 차이 경고를 제출하면 통과.

흔한 오해와 실패 복구

  1. Oracle 빈문자열 취급과 SQLite 빈문자열은 다르다.
  2. Python re는 여기 ASCII 숫자 패턴 검산만 담당하며 Oracle 정규식 엔진을 모사하지 않는다.

확인 문제

COALESCE(paid,0) 출력이0이면 원본 paid도 반드시0인가?

자기평가 · 자동채점 아님

평가 기준: 원본 NULL과0 두 경우를 제시하고 IS NULL 플래그로 구분하면 통과.

해설 보기

아니다. NULL도0으로 표시되어 구분 정보가 사라진다. 원본 paid IS NULL 열을 함께 남긴다.

독립 실습

105의 paid가 NULL인 경우와0인 경우를 구별하여 표시하라. 문자 금액 bad는0으로 저장하지 않게 하며 1월31일 다음날을 방언별로 작성하라. NVL2를 CASE로 바꾼 식과 Oracle/SQLite 자료형 변환 차이도 설명하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. NULL/0 구분, bad 격리, 다음날2026-02-01, NVL2 대응 CASE 및 자료형 차이 경고를 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: null-case
SELECT id,COALESCE(paid,0),CASE WHEN paid IS NOT NULL THEN 'known' ELSE 'unknown' END,NULLIF(paid,0),CASE WHEN paid IS NULL THEN '확인필요' WHEN paid>=10000 THEN '일반' ELSE '소액' END FROM booking WHERE id IN(103,105) ORDER BY id;
-- case: text-number-date
SELECT UPPER(TRIM(' ab ')),LOWER('XY'),substr('A-12',3,2),length('가람'),replace('A-12','-',''),abs(-3),round(12.345,2),5/2,5*1.0/2; SELECT date('2026-01-31','+1 day'),strftime('%Y-%m','2026-01-31'),julianday('2026-02-02')-julianday('2026-01-31'),CAST('12' AS INTEGER);
-- case: conversion-guard
SELECT CAST('bad' AS INTEGER),CASE WHEN py_match('^[0-9]+$','bad') THEN CAST('bad' AS INTEGER) ELSE NULL END,NULLIF(0,0),COALESCE(NULL,NULL,7);
-- case: variant-zero
UPDATE booking SET paid=0 WHERE id=105; SELECT id,paid IS NULL,NULLIF(paid,0),CASE paid WHEN NULL THEN 'null' ELSE 'other' END FROM booking WHERE id=105;

SELECT별 고정 정답과 오류 유형

{
  "null-case": [
    [
      [
        103,
        8000,
        "known",
        8000,
        "소액"
      ],
      [
        105,
        0,
        "unknown",
        null,
        "확인필요"
      ]
    ]
  ],
  "text-number-date": [
    [
      [
        "AB",
        "xy",
        "12",
        2,
        "A12",
        3,
        12.35,
        2,
        2.5
      ]
    ],
    [
      [
        "2026-02-01",
        "2026-01",
        2.0,
        12
      ]
    ]
  ],
  "conversion-guard": [
    [
      [
        0,
        null,
        null,
        7
      ]
    ]
  ],
  "variant-zero": [
    [
      [
        105,
        0,
        null,
        "other"
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE fx(id NUMBER PRIMARY KEY,paid NUMBER);
INSERT INTO fx VALUES(103,8000); INSERT INTO fx VALUES(105,NULL); COMMIT;
SELECT id,NVL(paid,0),NVL2(paid,'known','unknown'),NULLIF(paid,0),COALESCE(paid,7),CASE WHEN paid IS NULL THEN 'check' ELSE 'known' END FROM fx ORDER BY id;
SELECT UPPER(TRIM(' ab ')),LOWER('XY'),SUBSTR('A-12',3,2),LENGTH('AB'),REPLACE('A-12','-',''),ABS(-3),ROUND(12.345,2),5/2 FROM dual;
SELECT TO_CHAR(TO_DATE('2026-01-31','YYYY-MM-DD')+1,'YYYY-MM-DD'),TO_NUMBER('12'),CAST('12' AS NUMBER) FROM dual;
-- Expected by reasoning: 103/8000/known/8000/8000/known; 105/0/unknown/NULL/7/check.
-- Oracle number division 5/2=2.5 unlike SQLite integer division. Real Oracle NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

null-case|[[[103,8000,"known",8000,"소액"],[105,0,"unknown",null,"확인필요"]]]
text-number-date|[[["AB","xy","12",2,"A12",3,12.35,2,2.5]],[["2026-02-01","2026-01",2.0,12]]]
conversion-guard|[[[0,null,null,7]]]
variant-zero|[[[105,0,null,"other"]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

CASE WHEN paid IS NULL THEN '확인필요' WHEN paid=0 THEN '실제0' ELSE '유효금액' END. bad는 py_match 통과 전 CAST 금지. SQLite date('2026-01-31','+1 day')는2026-02-01, Oracle DATE '2026-01-31'+1도 다음날이며 TO_CHAR로 표시 형식을 정한다. comparison.oracle.sql 참고(미실행).

평가 기준: NULL/0 구분, bad 격리, 다음날2026-02-01, NVL2 대응 CASE 및 자료형 차이 경고를 제출하면 통과.

새 맥락에 적용하기

비민감 센서 문자열에 숫자·bad·NULL·0을 넣고 검증/변환/표시 단계를 분리하라. 오류와0을 합치면 재실습.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “함수는 값을 바꾸지만 모르는 사실을 알아내지는 못한다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

묶어 셀 때 분모부터

해결할 문제와 목표

접수 건수를 좌석 수로, 미확인을0원으로 혼동했다.

COUNT·SUM·AVG·MIN·MAX와 WHERE/HAVING의 입력 차이를 검산한다.

먼저 알아둘 것

  1. 빈칸·문자·날짜를 뜻대로 바꾸기의 예측·실행·오류복구 실습

핵심 한 문장: 행을 먼저 고른 다음 묶고, 묶은 값을 다시 고른다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

봉투별 서류를 묶는다.

비유가 닿지 않는 곳: 봉투 수·서류 수·서류에 적힌 인원 합은 서로 다르다.

실제로 작동하는 원리

설명용 논리 순서는 FROM/JOIN→WHERE→GROUP BY와 집계→HAVING→SELECT→DISTINCT→ORDER BY→행 제한이다. 물리 실행계획의 실제 순서를 강요하는 규칙은 아니다. 완료 필터의 중간행은101(10,2,12000),103(20,1,8000),104(30,3,15000). class별 COUNT=1,1,1; seats SUM=2,1,3. HAVING SUM(seats)>=2는10과30만 남긴다. COUNT(*)는 행을 모두, COUNT(paid)는 비NULL만 센다. class20 전체 접수는103/105 두 행이지만 paid 있는 건1개, SUM=8000,AVG=8000이다. AVG(COALESCE(paid,0))=4000은 미확인을0으로 가정한 다른 지표다. 모두 NULL이면 SUM/AVG는 NULL이며 COUNT(paid)=0이다. LEFT JOIN에서 접수 없는40도 NULL 확장행1개라 COUNT(*)=1이 된다. COUNT(b.id)는0이고 COALESCE(SUM(b.seats),0)는 표시용0이다. SQLite는 집계 쿼리에서 그룹에 없는 열을 허용하는 경우가 있지만 임의 대표값에 기대지 않는다. Oracle 호환을 위해 비집계 표시 열은 GROUP BY에 명시한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.

입력 예제

SELECT id,class_id,seats FROM booking WHERE status='완료' ORDER BY id; SELECT class_id,COUNT(*),SUM(seats) FROM booking WHERE status='완료' GROUP BY class_id HAVING SUM(seats)>=2 ORDER BY class_id;

예상 결과

[[[101, 10, 2], [103, 20, 1], [104, 30, 3]], [[10, 1, 2], [30, 1, 3]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 기본3개 금액합, 변경 후20의2/1/8000/2, NULL 제외와 미확인 건수 보고를 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. WHERE status 조건을 HAVING status로 옮기면 그룹 입력을 제대로 제한하지 못한다. 집계되지 않은 status에 기대지 않는다.
  2. JOIN 후 증폭 여부를 원본 완료 seats 합6과 대조한다.

확인 문제

class20에서 AVG(paid)와 AVG(COALESCE(paid,0))는 왜 다른가?

자기평가 · 자동채점 아님

평가 기준: 입력 두 값, 서로 다른 분모1/2, 결과8000/4000 및 미확인≠0 의미를 설명하면 통과.

해설 보기

원본8000,NULL에서 전자는 비NULL1개만 분모로8000, 후자는8000,0 두 개를 분모로4000이다.

독립 실습

완료 접수의 강좌별 paid 합을 구하라. 이어105의 상태만 완료로 바꿨을 때 class20의 건수·금액확인건수·금액합·좌석합을 예측하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 기본3개 금액합, 변경 후20의2/1/8000/2, NULL 제외와 미확인 건수 보고를 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: group-steps
SELECT id,class_id,seats FROM booking WHERE status='완료' ORDER BY id; SELECT class_id,COUNT(*),SUM(seats) FROM booking WHERE status='완료' GROUP BY class_id HAVING SUM(seats)>=2 ORDER BY class_id;
-- case: null-aggregates
SELECT class_id,COUNT(*),COUNT(paid),SUM(paid),AVG(paid),MIN(paid),MAX(paid),AVG(COALESCE(paid,0)) FROM booking WHERE class_id=20 GROUP BY class_id; SELECT SUM(paid),COUNT(paid) FROM booking WHERE id=105;
-- case: independent-paid
SELECT class_id,SUM(paid) FROM booking WHERE status='완료' GROUP BY class_id ORDER BY class_id;
-- case: zero-preserved
SELECT c.id,COUNT(*),COUNT(b.id),COALESCE(SUM(b.seats),0) FROM class c LEFT JOIN booking b ON b.class_id=c.id AND b.status='완료' GROUP BY c.id ORDER BY c.id;
-- case: variant-complete-null
UPDATE booking SET status='완료' WHERE id=105; SELECT class_id,COUNT(*),COUNT(paid),SUM(paid),SUM(seats) FROM booking WHERE status='완료' AND class_id=20 GROUP BY class_id;

SELECT별 고정 정답과 오류 유형

{
  "group-steps": [
    [
      [
        101,
        10,
        2
      ],
      [
        103,
        20,
        1
      ],
      [
        104,
        30,
        3
      ]
    ],
    [
      [
        10,
        1,
        2
      ],
      [
        30,
        1,
        3
      ]
    ]
  ],
  "null-aggregates": [
    [
      [
        20,
        2,
        1,
        8000,
        8000.0,
        8000,
        8000,
        4000.0
      ]
    ],
    [
      [
        null,
        0
      ]
    ]
  ],
  "independent-paid": [
    [
      [
        10,
        12000
      ],
      [
        20,
        8000
      ],
      [
        30,
        15000
      ]
    ]
  ],
  "zero-preserved": [
    [
      [
        10,
        1,
        1,
        2
      ],
      [
        20,
        1,
        1,
        1
      ],
      [
        30,
        1,
        1,
        3
      ],
      [
        40,
        1,
        0,
        0
      ]
    ]
  ],
  "variant-complete-null": [
    [
      [
        20,
        2,
        1,
        8000,
        2
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

실제 검산한 Python stdout (SQLite; Oracle 제외)

group-steps|[[[101,10,2],[103,20,1],[104,30,3]],[[10,1,2],[30,1,3]]]
null-aggregates|[[[20,2,1,8000,8000.0,8000,8000,4000.0]],[[null,0]]]
independent-paid|[[[10,12000],[20,8000],[30,15000]]]
zero-preserved|[[[10,1,1,2],[20,1,1,1],[30,1,1,3],[40,1,0,0]]]
variant-complete-null|[[[20,2,1,8000,2]]]
PASS 5 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

SELECT class_id,SUM(paid) FROM booking WHERE status='완료' GROUP BY class_id ORDER BY class_id; 기본10=12000,20=8000,30=15000.105 완료 변경 후20은 COUNT(*)2,COUNT(paid)1,SUM(paid)8000,SUM(seats)2. NULL 금액은 합계에서 제외되므로 보고서에 미확인1건도 함께 표기한다.

평가 기준: 기본3개 금액합, 변경 후20의2/1/8000/2, NULL 제외와 미확인 건수 보고를 모두 제출하면 통과.

새 맥락에 적용하기

가상 시설 예약의 신청건수·인원합·결제확인건수 세 지표를 한 보고서로 작성하라. NULL 포함 분모를 명시해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “행을 먼저 고른 다음 묶고, 묶은 값을 다시 고른다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

빈 강좌도 빠짐없이 찾기

해결할 문제와 목표

조인을 바꾸자 행이 사라지거나 같은 금액이 여러 번 더해졌다.

등가·비등가·SELF·INNER·CROSS·LEFT·RIGHT·FULL·USING·NATURAL을 실제 행수로 비교한다.

먼저 알아둘 것

  1. 묶어 셀 때 분모부터의 예측·실행·오류복구 실습

핵심 한 문장: 조인은 조건이 참인 모든 짝을 만들고 외부조인은 못 만난 쪽도 남긴다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

명부 두 장의 공통 번호로 짝을 맞춘다.

비유가 닿지 않는 곳: 한 번호가 여러 번 나오면 짝도 여러 개이며 NULL끼리는 등호로 만날 수 없다.

실제로 작동하는 원리

핵심 비교용 jl=(1,a),(2,b),(2,x), jr=(2,b),(3,c). ON l.k=r.k 등가 INNER는(2,b,b),(2,x,b)2행이다. LEFT는 여기에(1,a,NULL)3행, RIGHT는(3,NULL,c)도 남겨3행, FULL은 양쪽 미매칭을 합쳐4행, CROSS는3×2=6행이다. Non-EQUI l.k<r.k는1<2,1<3,두2<3으로4행이다. 이름이 아니라 키를 연결한다. USING(k)는 공통 k 열을 하나로 표시하고 등가 ON과 같은2행을 만든다. NATURAL은 현재 공통열 k뿐 아니라 label도 비교하여(2,b)1행이다. 나중에 동명열을 추가하면 조인 조건도 몰래 바뀌므로 실무에선 명시 ON/USING을 선호한다. SELF JOIN은 같은 category에 자식c/부모p 별칭을 주는 것으로 새 종류의 연산자는 아니다. 원 강좌 데이터는 class4행/booking5행, INNER는5행, LEFT는 예약 없는40까지6행, RIGHT는 모든 booking이 유효 FK라5행, FULL는6행이다. 완료 조건을 ON에 넣은 LEFT는10/20/30의 완료1개와40의 NULL로4행. WHERE b.status='완료'는 NULL 확장40을 탈락시켜3행이다. SQLite3.39부터 RIGHT/FULL을 지원하므로 이번3.53.1에서 둘 다 실제 실행한다. NULL 키를 서로 연결하려고 COALESCE를 무조건 쓰면 서로 다른 미확인 대상을 같은 키로 오인할 수 있다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.

입력 예제

SELECT l.k,l.label,r.k,r.label FROM jl l FULL OUTER JOIN jr r ON l.k=r.k ORDER BY COALESCE(l.k,r.k),l.label; SELECT (SELECT COUNT(*) FROM jl JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl LEFT JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl RIGHT JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl FULL JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl CROSS JOIN jr),(SELECT COUNT(*) FROM jl JOIN jr ON jl.k<jr.k);

예상 결과

[[[1, "a", null, null], [2, "b", 2, "b"], [2, "x", 2, "b"], [null, null, 3, "c"]], [[2, 3, 3, 4, 6, 4]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 6종 행수, FULL의4개 실제행, 변형4/6행의 곱셈 근거, SELF JOIN 부모 만들기를 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. 키의 양쪽 중복을 먼저 센다. DISTINCT로 증폭을 숨기지 않는다.
  2. NATURAL은 모든 동명열을 쓴다. 필터를 ON에서 WHERE로 옮기면 OUTER 의미가 달라진다.

확인 문제

LEFT JOIN 뒤 WHERE b.status='완료'를 쓰면 class40은 어떻게 되는가?

자기평가 · 자동채점 아님

평가 기준: 40 탈락 이유(UNKNOWN)·ON 위치·COUNT(b.id)=0 세 가지가 있어야 통과.

해설 보기

NULL 확장행의 status 비교는 UNKNOWN이라40이 탈락한다. ON으로 조건을 옮기고 COUNT(b.id)를 쓰면40의 완료건수0을 보존한다.

독립 실습

jl/jr 각 조인의 행수를 예측하고 FULL의 실제4행을 쓰라. jr에(2,z)를 추가하면 INNER/FULL은 몇 행인가? category2의 부모명도 SELF JOIN으로 찾으라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 6종 행수, FULL의4개 실제행, 변형4/6행의 곱셈 근거, SELF JOIN 부모 만들기를 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE jl(k INTEGER,label TEXT); CREATE TABLE jr(k INTEGER,label TEXT); INSERT INTO jl VALUES(1,'a'),(2,'b'),(2,'x'); INSERT INTO jr VALUES(2,'b'),(3,'c');

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: all-joins
SELECT l.k,l.label,r.k,r.label FROM jl l FULL OUTER JOIN jr r ON l.k=r.k ORDER BY COALESCE(l.k,r.k),l.label; SELECT (SELECT COUNT(*) FROM jl JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl LEFT JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl RIGHT JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl FULL JOIN jr ON jl.k=jr.k),(SELECT COUNT(*) FROM jl CROSS JOIN jr),(SELECT COUNT(*) FROM jl JOIN jr ON jl.k<jr.k);
-- case: natural-using
SELECT k,jl.label,jr.label FROM jl JOIN jr USING(k) ORDER BY jl.label; SELECT * FROM jl NATURAL JOIN jr ORDER BY k;
-- case: class-counts
SELECT (SELECT COUNT(*) FROM class c INNER JOIN booking b ON c.id=b.class_id),(SELECT COUNT(*) FROM class c LEFT JOIN booking b ON c.id=b.class_id),(SELECT COUNT(*) FROM class c RIGHT JOIN booking b ON c.id=b.class_id),(SELECT COUNT(*) FROM class c FULL JOIN booking b ON c.id=b.class_id);
-- case: on-versus-where
SELECT c.id,COUNT(b.id) FROM class c LEFT JOIN booking b ON c.id=b.class_id AND b.status='완료' GROUP BY c.id ORDER BY c.id; SELECT c.id FROM class c LEFT JOIN booking b ON c.id=b.class_id WHERE b.status='완료' ORDER BY c.id; SELECT c.id,c.name,p.name FROM category c LEFT JOIN category p ON c.parent_id=p.id WHERE c.id=2;
-- case: variant-right-duplicate
INSERT INTO jr VALUES(2,'z'); SELECT COUNT(*) FROM jl JOIN jr ON jl.k=jr.k; SELECT COUNT(*) FROM jl FULL JOIN jr ON jl.k=jr.k;

SELECT별 고정 정답과 오류 유형

{
  "all-joins": [
    [
      [
        1,
        "a",
        null,
        null
      ],
      [
        2,
        "b",
        2,
        "b"
      ],
      [
        2,
        "x",
        2,
        "b"
      ],
      [
        null,
        null,
        3,
        "c"
      ]
    ],
    [
      [
        2,
        3,
        3,
        4,
        6,
        4
      ]
    ]
  ],
  "natural-using": [
    [
      [
        2,
        "b",
        "b"
      ],
      [
        2,
        "x",
        "b"
      ]
    ],
    [
      [
        2,
        "b"
      ]
    ]
  ],
  "class-counts": [
    [
      [
        5,
        6,
        5,
        6
      ]
    ]
  ],
  "on-versus-where": [
    [
      [
        10,
        1
      ],
      [
        20,
        1
      ],
      [
        30,
        1
      ],
      [
        40,
        0
      ]
    ],
    [
      [
        10
      ],
      [
        20
      ],
      [
        30
      ]
    ],
    [
      [
        2,
        "목공",
        "만들기"
      ]
    ]
  ],
  "variant-right-duplicate": [
    [
      [
        4
      ]
    ],
    [
      [
        6
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

실제 검산한 Python stdout (SQLite; Oracle 제외)

all-joins|[[[1,"a",null,null],[2,"b",2,"b"],[2,"x",2,"b"],[null,null,3,"c"]],[[2,3,3,4,6,4]]]
natural-using|[[[2,"b","b"],[2,"x","b"]],[[2,"b"]]]
class-counts|[[[5,6,5,6]]]
on-versus-where|[[[10,1],[20,1],[30,1],[40,0]],[[10],[20],[30]],[[2,"목공","만들기"]]]
variant-right-duplicate|[[[4]],[[6]]]
PASS 5 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

기본 INNER/LEFT/RIGHT/FULL/CROSS/Non-EQUI=2/3/3/4/6/4. jr의2가2개이면 왼쪽2 두 개와2×2=4짝, FULL은 양쪽 미매칭2개를 더해6행. category2 목공의 부모는1 만들기다.

평가 기준: 6종 행수, FULL의4개 실제행, 변형4/6행의 곱셈 근거, SELF JOIN 부모 만들기를 모두 제출하면 통과.

새 맥락에 적용하기

가상 공구와 수리기록에 수리0건 공구를 남기는 보고서를 작성하라. 동일 공구번호 수리2건 추가 시 행수/합계 증가를 검산해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “조인은 조건이 참인 모든 짝을 만들고 외부조인은 못 만난 쪽도 남긴다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

질문 안에 작은 질문

해결할 문제와 목표

완료 접수가 있는 강좌를 찾다가 접수 수만큼 강좌가 중복되었다.

단일행·다중행·다중컬럼·상관·스칼라·인라인 뷰·EXISTS·View를 선택한다.

먼저 알아둘 것

  1. 빈 강좌도 빠짐없이 찾기의 예측·실행·오류복구 실습

핵심 한 문장: 서브쿼리는 값 하나·값 목록·행 집합 중 필요한 모양으로 작은 질문을 돌려준다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

창구가 참고 창구에 먼저 묻는다.

비유가 닿지 않는 곳: 항상 실제로 바깥 행마다 반복 실행하는 것은 아니며 최적화기가 다른 계획을 택할 수 있다.

실제로 작동하는 원리

단일행 예: paid>(SELECT AVG(paid) FROM booking)는 평균11750보다 큰101,102,104를 고른다. 다중행은 class_id IN(SELECT id FROM class WHERE category_id=4)로30을 찾는다. 다중컬럼은 (class_id,seats) IN(SELECT class_id,MAX(seats) ... GROUP BY class_id)처럼 한 쌍을 비교한다; class20의103/105는 좌석1 동점이므로 둘 다 나온다. 상관 EXISTS는 바깥 c.id와 안쪽 b.class_id를 연결한다. SELECT 1의1은 금액이 아니라 존재 검사용 표현이다. 완료가 하나라도 있는10,20,30은 각1행, NOT EXISTS는40. 상관조건을 빼면 완료가 전체에 하나만 있어도 모든 class가 통과한다. 스칼라 서브쿼리는 한 행 한 열 자리다. 0행이면 NULL; 여러 행이면 Oracle은 오류, SQLite는 첫 행 값을 사용하므로 SQLite에서 조용히 실행되었다고 올바른 스칼라 조건이라 단정하면 안 된다. 실제 과제는 MAX/COUNT 또는 UNIQUE 조건으로 단일행을 보장한다. FROM 안 SELECT는 인라인 뷰; 합계를 만든 뒤 바깥에서 필터한다. CREATE VIEW는 쿼리 정의를 저장하며 결과를 복사한 백업이 아니다. 원본 변경 후 뷰 결과도 바뀐다. NOT IN에 NULL이 있으면 “목록 모든 값과 다르다”의 일부가 UNKNOWN이 된다. blocked=(10,NULL)이면 id NOT IN blocked는 결과0행. NOT EXISTS의 키 비교는10만 제외하고20/30/40을 남긴다. NULL을 지운 NOT IN 또는 업무 의미에 맞는 NOT EXISTS로 복구한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.

입력 예제

SELECT c.id FROM class c WHERE EXISTS(SELECT 1 FROM booking b WHERE b.class_id=c.id AND b.status='완료') ORDER BY c.id; SELECT c.id FROM class c WHERE NOT EXISTS(SELECT 1 FROM booking b WHERE b.class_id=c.id AND b.status='완료') ORDER BY c.id;

예상 결과

[[[10], [20], [30]], [[40]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 기본40,102 변형 후40, 상관조건 c.id=b.class_id, NOT IN 실패0행과 복구20/30/40을 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. 스칼라 다중행이 SQLite에서 오류 없었다고 Oracle 호환으로 판정하지 않는다.
  2. VIEW는 SELECT 정의이지 스냅샷·백업이 아니다.

확인 문제

EXISTS 안 SELECT 1의1은 접수 금액인가? 왜 JOIN처럼 강좌가 중복되지 않는가?

자기평가 · 자동채점 아님

평가 기준: 상수의 의미, 바깥 한 행당 존재 판단, class10의 접수2개에도 출력1행을 설명하면 통과.

해설 보기

1은 존재를 판단하기 위한 상수다. 바깥 class 각 행에 대해 조건 참/거짓만 결정하므로 접수 개수만큼 복제하지 않는다.

독립 실습

완료 접수가 없는 강좌를 NOT EXISTS로 찾으라.102를 완료로 바꿔도 답이 바뀌는지 확인하고, blocked의 NULL 때문에 NOT IN이 실패하는 반례를 고쳐라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 기본40,102 변형 후40, 상관조건 c.id=b.class_id, NOT IN 실패0행과 복구20/30/40을 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: exists
SELECT c.id FROM class c WHERE EXISTS(SELECT 1 FROM booking b WHERE b.class_id=c.id AND b.status='완료') ORDER BY c.id; SELECT c.id FROM class c WHERE NOT EXISTS(SELECT 1 FROM booking b WHERE b.class_id=c.id AND b.status='완료') ORDER BY c.id;
-- case: single-multi-tuple
SELECT AVG(paid) FROM booking; SELECT id FROM booking WHERE paid>(SELECT AVG(paid) FROM booking) ORDER BY id; SELECT id FROM booking WHERE class_id IN(SELECT id FROM class WHERE category_id=4) ORDER BY id; SELECT id FROM booking WHERE (class_id,seats) IN(SELECT class_id,MAX(seats) FROM booking GROUP BY class_id) ORDER BY id;
-- case: scalar-inline
SELECT c.id,(SELECT MAX(b.paid) FROM booking b WHERE b.class_id=c.id) FROM class c ORDER BY c.id; SELECT class_id,n FROM (SELECT class_id,SUM(seats) n FROM booking WHERE status='완료' GROUP BY class_id) WHERE n>=2 ORDER BY class_id;
-- case: null-not-in
CREATE TABLE blocked(id INTEGER); INSERT INTO blocked VALUES(10),(NULL); SELECT id FROM class WHERE id NOT IN(SELECT id FROM blocked) ORDER BY id; SELECT c.id FROM class c WHERE NOT EXISTS(SELECT 1 FROM blocked b WHERE b.id=c.id) ORDER BY c.id;
-- case: view-variant
CREATE VIEW completed AS SELECT id,class_id,seats FROM booking WHERE status='완료'; SELECT COUNT(*) FROM completed; UPDATE booking SET status='완료' WHERE id=102; SELECT COUNT(*) FROM completed; SELECT c.id FROM class c WHERE NOT EXISTS(SELECT 1 FROM booking b WHERE b.class_id=c.id AND b.status='완료') ORDER BY c.id;
-- case: scalar-cardinality
SELECT (SELECT paid FROM booking WHERE id=999); SELECT (SELECT paid FROM booking WHERE class_id=20 ORDER BY id); SELECT COUNT(*) FROM booking WHERE class_id=20;

SELECT별 고정 정답과 오류 유형

{
  "exists": [
    [
      [
        10
      ],
      [
        20
      ],
      [
        30
      ]
    ],
    [
      [
        40
      ]
    ]
  ],
  "single-multi-tuple": [
    [
      [
        11750.0
      ]
    ],
    [
      [
        101
      ],
      [
        102
      ],
      [
        104
      ]
    ],
    [
      [
        104
      ]
    ],
    [
      [
        101
      ],
      [
        103
      ],
      [
        104
      ],
      [
        105
      ]
    ]
  ],
  "scalar-inline": [
    [
      [
        10,
        12000
      ],
      [
        20,
        8000
      ],
      [
        30,
        15000
      ],
      [
        40,
        null
      ]
    ],
    [
      [
        10,
        2
      ],
      [
        30,
        3
      ]
    ]
  ],
  "null-not-in": [
    [],
    [
      [
        20
      ],
      [
        30
      ],
      [
        40
      ]
    ]
  ],
  "view-variant": [
    [
      [
        3
      ]
    ],
    [
      [
        4
      ]
    ],
    [
      [
        40
      ]
    ]
  ],
  "scalar-cardinality": [
    [
      [
        null
      ]
    ],
    [
      [
        8000
      ]
    ],
    [
      [
        2
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

실제 검산한 Python stdout (SQLite; Oracle 제외)

exists|[[[10],[20],[30]],[[40]]]
single-multi-tuple|[[[11750.0]],[[101],[102],[104]],[[104]],[[101],[103],[104],[105]]]
scalar-inline|[[[10,12000],[20,8000],[30,15000],[40,null]],[[10,2],[30,3]]]
null-not-in|[[],[[20],[30],[40]]]
view-variant|[[[3]],[[4]],[[40]]]
scalar-cardinality|[[[null]],[[8000]],[[2]]]
PASS 6 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

SELECT c.id FROM class c WHERE NOT EXISTS(SELECT 1 FROM booking b WHERE b.class_id=c.id AND b.status='완료') ORDER BY c.id; 답40,102 변경 후에도40. blocked는 NOT EXISTS로 바꾸면20/30/40. SELECT id FROM class WHERE id NOT IN(SELECT id FROM blocked WHERE id IS NOT NULL)도 이 비NULL class 키 조건에서 같은 결과다.

평가 기준: 기본40,102 변형 후40, 상관조건 c.id=b.class_id, NOT IN 실패0행과 복구20/30/40을 모두 제출하면 통과.

새 맥락에 적용하기

대여기록이 없는 공구를 NOT EXISTS로 찾고 NULL 반납기록을 더한 반례를 쓰라. 공구 키와 대여 키를 혼동하지 않아야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “서브쿼리는 값 하나·값 목록·행 집합 중 필요한 모양으로 작은 질문을 돌려준다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

합치고 겹치고 빼기

해결할 문제와 목표

두 모집명부를 더한 뒤 중복 신청을 건수로 오인했다.

UNION·UNION ALL·INTERSECT·EXCEPT와 Oracle MINUS의 중복/열 계약을 검증한다.

먼저 알아둘 것

  1. 질문 안에 작은 질문의 예측·실행·오류복구 실습

핵심 한 문장: 집합 연산은 행 전체를 비교하고 UNION ALL만 중복을 그대로 보존한다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

두 명부를 포개거나 겹친 부분만 남긴다.

비유가 닿지 않는 곳: 같은 사람이더라도 다른 열 값을 선택하면 다른 행이며 정렬은 별도다.

실제로 작동하는 원리

a=(1,1,2,NULL), b=(2,3,NULL). UNION은 NULL,1,2,3의4행, UNION ALL은7행, INTERSECT는 NULL,2의2행, a EXCEPT b는1 한 행이다. 집합 중복제거에서 NULL끼리는 중복으로 취급되며 WHERE NULL=NULL의 UNKNOWN과 목적이 다르다. EXCEPT는 SQLite 차집합, Oracle19c에선 MINUS. 두 쿼리는 열 개수가 같아야 하며 Oracle은 대응 열의 자료형 그룹도 맞아야 한다. SQLite의 느슨한 타입에 기대어 숫자와 문자열을 섞지 않는다. 중복제거 기준은 모든 선택 열이다. 이름만 고르면 동명이인을 합칠 수 있고 id와 상태를 고르면 같은 id의 서로 다른 상태는 남는다. 최종 ORDER BY로 출력 순서를 정한다. 여러 집합 연산을 섞으면 우선순위 착각을 막기 위해 CTE/인라인 뷰로 단계 이름을 붙인다. Oracle19c 문서는 좌결합을 설명하지만 앞으로 다른 우선순위 가능성도 경고하므로 명시 구조가 안전하다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

SELECT v FROM aa UNION SELECT v FROM bb ORDER BY v; SELECT v FROM aa UNION ALL SELECT v FROM bb ORDER BY v; SELECT v FROM aa INTERSECT SELECT v FROM bb ORDER BY v; SELECT v FROM aa EXCEPT SELECT v FROM bb ORDER BY v;

예상 결과

[[[null], [1], [2], [3]], [[null], [null], [1], [1], [2], [2], [3]], [[null], [2]], [[1]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 기본1, 변형0행/8행, 열수 오류의 원인과 수정, Oracle MINUS 대조가 모두 있어야 통과.

흔한 오해와 실패 복구

  1. 집합 연산의 이름만 외우지 말고 전체 행과 중복을 적는다.
  2. 정렬하지 않은 출력을 고정 정답 순서로 가정하지 않는다.

확인 문제

UNION에서 NULL 두 개가 하나가 되는 것은 NULL=NULL이 TRUE라는 뜻인가?

자기평가 · 자동채점 아님

평가 기준: UNION NULL1행과 WHERE NULL=NULL0행을 구분하면 통과.

해설 보기

아니다. 중복제거의 NULL 동등 취급과 WHERE 비교의3값 논리는 다른 규칙이다.

독립 실습

a에만 있는 값을 구하고 bb에1을 넣은 뒤 차집합/UNION ALL 행수를 예측하라. 두 열을 실수로 SELECT한 오류도 복구하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 기본1, 변형0행/8행, 열수 오류의 원인과 수정, Oracle MINUS 대조가 모두 있어야 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE aa(v INTEGER); CREATE TABLE bb(v INTEGER); INSERT INTO aa VALUES(1),(1),(2),(NULL); INSERT INTO bb VALUES(2),(3),(NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: four-sets
SELECT v FROM aa UNION SELECT v FROM bb ORDER BY v; SELECT v FROM aa UNION ALL SELECT v FROM bb ORDER BY v; SELECT v FROM aa INTERSECT SELECT v FROM bb ORDER BY v; SELECT v FROM aa EXCEPT SELECT v FROM bb ORDER BY v;
-- case: column-error-recovery
SELECT v FROM aa UNION SELECT v,1 FROM bb; SELECT v FROM aa UNION SELECT v FROM bb ORDER BY v;
-- case: variant-duplicate
INSERT INTO bb VALUES(1); SELECT v FROM aa EXCEPT SELECT v FROM bb ORDER BY v; SELECT COUNT(*) FROM (SELECT v FROM aa UNION ALL SELECT v FROM bb);

SELECT별 고정 정답과 오류 유형

{
  "four-sets": [
    [
      [
        null
      ],
      [
        1
      ],
      [
        2
      ],
      [
        3
      ]
    ],
    [
      [
        null
      ],
      [
        null
      ],
      [
        1
      ],
      [
        1
      ],
      [
        2
      ],
      [
        2
      ],
      [
        3
      ]
    ],
    [
      [
        null
      ],
      [
        2
      ]
    ],
    [
      [
        1
      ]
    ]
  ],
  "column-error-recovery": [
    "ERROR OperationalError",
    [
      [
        null
      ],
      [
        1
      ],
      [
        2
      ],
      [
        3
      ]
    ]
  ],
  "variant-duplicate": [
    [],
    [
      [
        8
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE aa(v NUMBER); CREATE TABLE bb(v NUMBER);
INSERT INTO aa VALUES(1); INSERT INTO aa VALUES(1); INSERT INTO aa VALUES(2); INSERT INTO aa VALUES(NULL);
INSERT INTO bb VALUES(2); INSERT INTO bb VALUES(3); INSERT INTO bb VALUES(NULL); COMMIT;
SELECT v FROM aa UNION SELECT v FROM bb ORDER BY v NULLS FIRST;
SELECT v FROM aa UNION ALL SELECT v FROM bb ORDER BY v NULLS FIRST;
SELECT v FROM aa INTERSECT SELECT v FROM bb ORDER BY v NULLS FIRST;
SELECT v FROM aa MINUS SELECT v FROM bb ORDER BY v NULLS FIRST;
-- Expected respectively 4/7/2/1 rows; NULL ordering explicitly matched to SQLite. NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

four-sets|[[[null],[1],[2],[3]],[[null],[null],[1],[1],[2],[2],[3]],[[null],[2]],[[1]]]
column-error-recovery|["ERROR OperationalError",[[null],[1],[2],[3]]]
variant-duplicate|[[],[[8]]]
PASS 3 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

a EXCEPT b는1. bb에1 추가 후 차집합0행, UNION ALL은8행이다. 양쪽 SELECT 열 개수를1로 맞춘다. Oracle19c는 같은 구조에서 EXCEPT 대신 MINUS를 쓴다.

평가 기준: 기본1, 변형0행/8행, 열수 오류의 원인과 수정, Oracle MINUS 대조가 모두 있어야 통과.

새 맥락에 적용하기

합성 동명이인 두 명을 이름만 UNION한 결과와 id/이름 UNION 결과를 비교하라. 잘못 합쳐진 사람을 식별해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “집합 연산은 행 전체를 비교하고 UNION ALL만 중복을 그대로 보존한다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

소계가 생긴 빈칸은 다른 빈칸

해결할 문제와 목표

지역 미확인의 NULL을 총계 행의 NULL과 합쳐 버렸다.

ROLLUP·CUBE·GROUPING SETS·GROUPING을 SQLite 등가 집계로 검산한다.

먼저 알아둘 것

  1. 합치고 겹치고 빼기의 예측·실행·오류복구 실습

핵심 한 문장: 소계는 새 그룹을 만든 행이며 상세 자료의 NULL과 구별하는 표지가 필요하다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

서랍별 합계와 전체 합계를 별도 메모지에 쓴다.

비유가 닿지 않는 곳: 모든 메모지를 다시 더하면 같은 원본을 여러 번 세는 셈이다.

실제로 작동하는 원리

sales의 중간 상세는 남/온라인5, 북/온라인2, 북/현장3이다. 지역합 남5/북5, 채널합 온라인7/현장3, 전체10. ROLLUP(region,channel)은 (region,channel),(region),() 순서의 그룹집합이라3+2+1=6행; CUBE는(channel)도 더해8행. GROUPING SETS((region),(channel))는 필요한 두 종류만 뽑아4행이다. ROLLUP 순서를 바꾸면 채널 소계가 생겨 의미가 달라진다. Oracle GROUPING(region)은 해당 열이 집계 수준 때문에 제거되면1, 실제 상세/그룹키 값이면 NULL이어도0이다. SQLite엔 이 구문이 없어 각 GROUP BY를 UNION ALL로 합치고 직접 gr/gc 플래그를 넣는다. 소계 종류가 다르므로 UNION의 중복제거로 우연히 합치면 안 된다. 원본에(NULL,온라인,4)를 추가하면 실제NULL 지역 소계는(gr=0,gc=1,NULL,NULL,4), 전체는(1,1,NULL,NULL,14)로 다르다. 합계 검증은 상세합=전체 한 행을 비교해야 하며 CUBE8행 전체를 합하면 이중계산한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

SELECT 0 gr,0 gc,region,channel,SUM(n) n FROM sales GROUP BY region,channel UNION ALL SELECT 0,1,region,NULL,SUM(n) FROM sales GROUP BY region UNION ALL SELECT 1,0,NULL,channel,SUM(n) FROM sales GROUP BY channel UNION ALL SELECT 1,1,NULL,NULL,SUM(n) FROM sales ORDER BY gr,gc,region,channel;

예상 결과

[[[0, 0, "남", "온라인", 5], [0, 0, "북", "온라인", 2], [0, 0, "북", "현장", 3], [0, 1, "남", null, 5], [0, 1, "북", null, 5], [1, 0, null, "온라인", 7], [1, 0, null, "현장", 3], [1, 1, null, null, 10]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 각 그룹집합의 열·행수·합계10, NULL변형4/14와 플래그0/1 대조를 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. SQLite UNION ALL 대조는 Oracle 엔진 실행 증거가 아니다.
  2. GROUPING 플래그 없이 NULL을 모두 총계라고 표시하지 않는다.

확인 문제

GROUPING(region)=1이면 원본에 지역 값이 없었다는 뜻인가? 본문 변형의 실제NULL 지역소계4와 총계14에서 각각 플래그를 제시하라.

자기평가 · 자동채점 아님

평가 기준: 집계NULL과 실제NULL을 플래그1/0 및 변형4/14 예로 구분하면 통과.

해설 보기

아니다. 집계 수준에서 region을 묶는 열에서 제외했다는 뜻이다. 원본NULL 지역 그룹은 GROUPING(region)=0이다.

독립 실습

ROLLUP6행/CUBE8행을 종이에 전개하고 실제NULL 지역4를 추가했을 때 지역소계와 총계의 gr/gc를 구분하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 각 그룹집합의 열·행수·합계10, NULL변형4/14와 플래그0/1 대조를 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE sales(region TEXT,channel TEXT,n INTEGER); INSERT INTO sales VALUES('북','온라인',2),('북','현장',3),('남','온라인',5);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: cube
SELECT 0 gr,0 gc,region,channel,SUM(n) n FROM sales GROUP BY region,channel UNION ALL SELECT 0,1,region,NULL,SUM(n) FROM sales GROUP BY region UNION ALL SELECT 1,0,NULL,channel,SUM(n) FROM sales GROUP BY channel UNION ALL SELECT 1,1,NULL,NULL,SUM(n) FROM sales ORDER BY gr,gc,region,channel;
-- case: rollup
SELECT 0 gr,0 gc,region,channel,SUM(n) n FROM sales GROUP BY region,channel UNION ALL SELECT 0,1,region,NULL,SUM(n) FROM sales GROUP BY region UNION ALL SELECT 1,1,NULL,NULL,SUM(n) FROM sales ORDER BY gr,gc,region,channel;
-- case: grouping-sets
SELECT 0 gr,1 gc,region,NULL channel,SUM(n) n FROM sales GROUP BY region UNION ALL SELECT 1,0,NULL,channel,SUM(n) FROM sales GROUP BY channel ORDER BY gr,gc,region,channel;
-- case: variant-real-null
INSERT INTO sales VALUES(NULL,'온라인',4); SELECT 0 gr,1 gc,region,NULL channel,SUM(n) n FROM sales GROUP BY region HAVING region IS NULL UNION ALL SELECT 1,1,NULL,NULL,SUM(n) FROM sales;

SELECT별 고정 정답과 오류 유형

{
  "cube": [
    [
      [
        0,
        0,
        "남",
        "온라인",
        5
      ],
      [
        0,
        0,
        "북",
        "온라인",
        2
      ],
      [
        0,
        0,
        "북",
        "현장",
        3
      ],
      [
        0,
        1,
        "남",
        null,
        5
      ],
      [
        0,
        1,
        "북",
        null,
        5
      ],
      [
        1,
        0,
        null,
        "온라인",
        7
      ],
      [
        1,
        0,
        null,
        "현장",
        3
      ],
      [
        1,
        1,
        null,
        null,
        10
      ]
    ]
  ],
  "rollup": [
    [
      [
        0,
        0,
        "남",
        "온라인",
        5
      ],
      [
        0,
        0,
        "북",
        "온라인",
        2
      ],
      [
        0,
        0,
        "북",
        "현장",
        3
      ],
      [
        0,
        1,
        "남",
        null,
        5
      ],
      [
        0,
        1,
        "북",
        null,
        5
      ],
      [
        1,
        1,
        null,
        null,
        10
      ]
    ]
  ],
  "grouping-sets": [
    [
      [
        0,
        1,
        "남",
        null,
        5
      ],
      [
        0,
        1,
        "북",
        null,
        5
      ],
      [
        1,
        0,
        null,
        "온라인",
        7
      ],
      [
        1,
        0,
        null,
        "현장",
        3
      ]
    ]
  ],
  "variant-real-null": [
    [
      [
        0,
        1,
        null,
        null,
        4
      ],
      [
        1,
        1,
        null,
        null,
        14
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE sales(region VARCHAR2(10),channel VARCHAR2(20),n NUMBER);
INSERT INTO sales VALUES('북','온라인',2); INSERT INTO sales VALUES('북','현장',3); INSERT INTO sales VALUES('남','온라인',5); COMMIT;
SELECT region,channel,SUM(n) n,GROUPING(region) gr,GROUPING(channel) gc FROM sales GROUP BY ROLLUP(region,channel) ORDER BY gr,gc,region NULLS FIRST,channel NULLS FIRST;
SELECT region,channel,SUM(n) n,GROUPING(region) gr,GROUPING(channel) gc FROM sales GROUP BY CUBE(region,channel) ORDER BY gr,gc,region NULLS FIRST,channel NULLS FIRST;
SELECT region,channel,SUM(n) n,GROUPING(region) gr,GROUPING(channel) gc FROM sales GROUP BY GROUPING SETS((region),(channel)) ORDER BY gr,gc,region NULLS FIRST,channel NULLS FIRST;
INSERT INTO sales VALUES(NULL,'온라인',4);
SELECT region,SUM(n),GROUPING(region) gr FROM sales GROUP BY ROLLUP(region) ORDER BY gr,region NULLS FIRST;
ROLLBACK;
-- Expected levels and totals described in lesson. Oracle NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

cube|[[[0,0,"남","온라인",5],[0,0,"북","온라인",2],[0,0,"북","현장",3],[0,1,"남",null,5],[0,1,"북",null,5],[1,0,null,"온라인",7],[1,0,null,"현장",3],[1,1,null,null,10]]]
rollup|[[[0,0,"남","온라인",5],[0,0,"북","온라인",2],[0,0,"북","현장",3],[0,1,"남",null,5],[0,1,"북",null,5],[1,1,null,null,10]]]
grouping-sets|[[[0,1,"남",null,5],[0,1,"북",null,5],[1,0,null,"온라인",7],[1,0,null,"현장",3]]]
variant-real-null|[[[0,1,null,null,4],[1,1,null,null,14]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

CUBE는 상세3,지역2,채널2,총계1; ROLLUP은 채널2를 제외한다. 변형 실NULL 지역소계4의 gr=0/gc=1, 총계14의 gr=1/gc=1. 실제NULL을 COALESCE만으로 총계라 표시하면 오답이다.

평가 기준: 각 그룹집합의 열·행수·합계10, NULL변형4/14와 플래그0/1 대조를 모두 제출하면 통과.

새 맥락에 적용하기

공구 종류/대여채널별 소계 요구를 받아 필요한 GROUPING SETS만 선택하라. 요청에 없는 총계를 넣지 않고 합계 검산 수준을 명시해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “소계는 새 그룹을 만든 행이며 상세 자료의 NULL과 구별하는 표지가 필요하다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

순위와 누적은 행을 남긴다

해결할 문제와 목표

동점 순위가 흔들리고 LAST_VALUE가 마지막 전체 값 대신 현재 값을 보인다.

순위·집계·전후값·프레임·비율·Top N을 재현 가능한 순서로 계산한다.

먼저 알아둘 것

  1. 소계가 생긴 빈칸은 다른 빈칸의 예측·실행·오류복구 실습

핵심 한 문장: GROUP BY는 행을 모으지만 OVER는 각 행을 남긴 채 주변을 계산한다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

모든 이름표를 남기고 등수와 누적 점수를 옆에 적는다.

비유가 닿지 않는 곳: 동점 처리·보는 범위·표시 순서를 따로 지정하지 않으면 결과가 의도와 달라진다.

실제로 작동하는 원리

score A=(id1,10),(id2,10),(id3,8), B=(id4,2). PARTITION BY grp는 A/B를 별도 계산한다. RANK는 A에서1,1,3; DENSE_RANK는1,1,2; ROW_NUMBER는 n DESC,id로1,2,3. 순위 함수에 id까지 넣으면10/10 동점 자체가 없어지므로 “등수”와 “고유 순번”의 ORDER BY는 목적에 맞게 나눈다. ROWS 누적은 고유 id 순서를 포함해10,20,28. RANGE n DESC 누적은 같은10 동점 전체가 현재 범위라20,20,28. 기본 프레임도 ORDER BY가 있으면 RANGE UNBOUNDED PRECEDING~CURRENT ROW여서 명시하지 않으면 착각하기 쉽다. 이동 AVG의 ROWS1 PRECEDING~CURRENT ROW는10,10,9. LAG=이전 n, LEAD=다음 n; 첫/끝 없으면NULL. FIRST_VALUE/LAST_VALUE는 현재 프레임 양끝이므로 전체 마지막8을 원하면 UNBOUNDED FOLLOWING까지 펼친다. LAG/LEAD는 프레임 집계와 달리 정렬된 파티션의 오프셋을 따른다. RATIO_TO_REPORT(n) OVER(PARTITION BY grp)는 A에서10/28,10/28,8/28이다. SQLite는1.0*n/NULLIF(SUM(n) OVER(...),0)로 이 비음수·비NULL 예제를 검산한다. 0합은 별도 NULL 정책을 명시하며 Oracle의 모든 타입/예외를 복제했다고 주장하지 않는다. Top N은 정확히N행인지 동점을 함께 뽑는지 먼저 정한다. ROW_NUMBER<=2는 A1/2, DENSE_RANK<=2는A1/2/3. SQLite ORDER BY ... LIMIT2와 Oracle ORDER BY ... FETCH FIRST2 ROWS ONLY를 대조한다. Oracle ROWNUM<=2 뒤 같은 SELECT의 ORDER BY는 먼저 고른 행만 정렬하므로 Top N 보장 안 됨; 안쪽 SELECT 정렬 후 바깥 ROWNUM 또는 행제한 절을 쓴다. ROWNUM>1은0행이다. OVER 내부 정렬만으로 최종 출력 순서를 보장하지 않아 마지막 ORDER BY도 둔다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

SELECT id,grp,n,
ROW_NUMBER() OVER(PARTITION BY grp ORDER BY n DESC,id) rn,
RANK() OVER(PARTITION BY grp ORDER BY n DESC) r,
DENSE_RANK() OVER(PARTITION BY grp ORDER BY n DESC) dr,
SUM(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) running,
SUM(n) OVER(PARTITION BY grp ORDER BY n DESC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) peers,
AVG(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) moving,
LAG(n) OVER(PARTITION BY grp ORDER BY n DESC,id) prev,
LEAD(n) OVER(PARTITION BY grp ORDER BY n DESC,id) next,
FIRST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) first_n,
LAST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) last_n,
ROUND(1.0*n/NULLIF(SUM(n) OVER(PARTITION BY grp),0),4) share
FROM score ORDER BY grp,id;

예상 결과

[[[1, "A", 10, 1, 1, 1, 10, 20, 10.0, null, 10, 10, 8, 0.3571], [2, "A", 10, 2, 1, 1, 20, 20, 10.0, 10, 8, 10, 8, 0.3571], [3, "A", 8, 3, 3, 2, 28, 28, 9.0, 10, null, 10, 8, 0.2857], [4, "B", 2, 1, 1, 1, 2, 2, 2.0, null, null, 2, 2, 1.0]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. Top2 30/10과좌석3/2, tie-breaker class_id, 변형순위1/1/1과1/2/3, LAST_VALUE 끝범위까지 제출하면 통과.

흔한 오해와 실패 복구

  1. WHERE에서 같은 SELECT의 윈도 결과 별칭을 필터하지 말고 CTE/인라인 뷰 바깥에서 거른다.
  2. RANK ORDER BY에 고유키를 더하면 동점 정책이 바뀐다.

확인 문제

ORDER BY 없이 LIMIT2를 쓰면 항상 같은 두 행을 얻는가? 동점이면 무엇을 추가해야 하는가? 정확히 두 행과 동점 포함 상위 등수의 선택 정책도 구별하라.

자기평가 · 자동채점 아님

평가 기준: 순서 비보장, n DESC,id의 고유 정렬, 동점포함 등수 정책과 정확히2행 정책 차이를 설명하면 통과.

해설 보기

보장되지 않는다. 목적 열의 ORDER BY와 고유키 tie-breaker를 추가해야 정확히 같은 두 행을 고를 수 있다.

독립 실습

완료 좌석합이 큰 강좌2개를 결정적 정렬로 찾으라. 이어 score3을10으로 바꾸고 RANK와 ROW_NUMBER를 비교하라. LAST_VALUE 전체 마지막8을 얻는 프레임을 쓰라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. Top2 30/10과좌석3/2, tie-breaker class_id, 변형순위1/1/1과1/2/3, LAST_VALUE 끝범위까지 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE score(id INTEGER PRIMARY KEY,grp TEXT,n INTEGER); INSERT INTO score VALUES(1,'A',10),(2,'A',10),(3,'A',8),(4,'B',2);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: window-all
SELECT id,grp,n,
ROW_NUMBER() OVER(PARTITION BY grp ORDER BY n DESC,id) rn,
RANK() OVER(PARTITION BY grp ORDER BY n DESC) r,
DENSE_RANK() OVER(PARTITION BY grp ORDER BY n DESC) dr,
SUM(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) running,
SUM(n) OVER(PARTITION BY grp ORDER BY n DESC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) peers,
AVG(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) moving,
LAG(n) OVER(PARTITION BY grp ORDER BY n DESC,id) prev,
LEAD(n) OVER(PARTITION BY grp ORDER BY n DESC,id) next,
FIRST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) first_n,
LAST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) last_n,
ROUND(1.0*n/NULLIF(SUM(n) OVER(PARTITION BY grp),0),4) share
FROM score ORDER BY grp,id;
-- case: last-frame
SELECT id,LAST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id),LAST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM score WHERE grp='A' ORDER BY id;
-- case: topn
SELECT id,n FROM score ORDER BY n DESC,id LIMIT 2; SELECT id FROM (SELECT id,grp,DENSE_RANK() OVER(PARTITION BY grp ORDER BY n DESC) dr FROM score) WHERE grp='A' AND dr<=2 ORDER BY id; SELECT class_id,SUM(seats) n FROM booking WHERE status='완료' GROUP BY class_id ORDER BY n DESC,class_id LIMIT 2;
-- case: variant-tie
UPDATE score SET n=10 WHERE id=3; SELECT id,RANK() OVER(PARTITION BY grp ORDER BY n DESC),ROW_NUMBER() OVER(PARTITION BY grp ORDER BY n DESC,id) FROM score WHERE grp='A' ORDER BY id;

SELECT별 고정 정답과 오류 유형

{
  "window-all": [
    [
      [
        1,
        "A",
        10,
        1,
        1,
        1,
        10,
        20,
        10.0,
        null,
        10,
        10,
        8,
        0.3571
      ],
      [
        2,
        "A",
        10,
        2,
        1,
        1,
        20,
        20,
        10.0,
        10,
        8,
        10,
        8,
        0.3571
      ],
      [
        3,
        "A",
        8,
        3,
        3,
        2,
        28,
        28,
        9.0,
        10,
        null,
        10,
        8,
        0.2857
      ],
      [
        4,
        "B",
        2,
        1,
        1,
        1,
        2,
        2,
        2.0,
        null,
        null,
        2,
        2,
        1.0
      ]
    ]
  ],
  "last-frame": [
    [
      [
        1,
        10,
        8
      ],
      [
        2,
        10,
        8
      ],
      [
        3,
        8,
        8
      ]
    ]
  ],
  "topn": [
    [
      [
        1,
        10
      ],
      [
        2,
        10
      ]
    ],
    [
      [
        1
      ],
      [
        2
      ],
      [
        3
      ]
    ],
    [
      [
        30,
        3
      ],
      [
        10,
        2
      ]
    ]
  ],
  "variant-tie": [
    [
      [
        1,
        1,
        1
      ],
      [
        2,
        1,
        2
      ],
      [
        3,
        1,
        3
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE score(id NUMBER PRIMARY KEY,grp VARCHAR2(5),n NUMBER);
INSERT INTO score VALUES(1,'A',10); INSERT INTO score VALUES(2,'A',10); INSERT INTO score VALUES(3,'A',8); INSERT INTO score VALUES(4,'B',2); COMMIT;
SELECT id,grp,n,
ROW_NUMBER() OVER(PARTITION BY grp ORDER BY n DESC,id) rn,
RANK() OVER(PARTITION BY grp ORDER BY n DESC) r,
DENSE_RANK() OVER(PARTITION BY grp ORDER BY n DESC) dr,
SUM(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) running,
SUM(n) OVER(PARTITION BY grp ORDER BY n DESC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) peers,
AVG(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) moving,
LAG(n) OVER(PARTITION BY grp ORDER BY n DESC,id) prev,
LEAD(n) OVER(PARTITION BY grp ORDER BY n DESC,id) next,
FIRST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) first_n,
LAST_VALUE(n) OVER(PARTITION BY grp ORDER BY n DESC,id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) last_n,
ROUND(RATIO_TO_REPORT(n) OVER(PARTITION BY grp),4) share
FROM score ORDER BY grp,id;
SELECT id,n FROM score ORDER BY n DESC,id FETCH FIRST 2 ROWS ONLY;
SELECT id,n FROM (SELECT id,n FROM score ORDER BY n DESC,id) WHERE ROWNUM<=2 ORDER BY n DESC,id;
SELECT id,n FROM score ORDER BY n DESC FETCH FIRST 1 ROW WITH TIES;
SELECT id FROM score WHERE ROWNUM>1;
-- Expected: first two queries id1,2; WITH TIES id1,2 (order among peers unspecified); ROWNUM>1 no rows. NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

window-all|[[[1,"A",10,1,1,1,10,20,10.0,null,10,10,8,0.3571],[2,"A",10,2,1,1,20,20,10.0,10,8,10,8,0.3571],[3,"A",8,3,3,2,28,28,9.0,10,null,10,8,0.2857],[4,"B",2,1,1,1,2,2,2.0,null,null,2,2,1.0]]]
last-frame|[[[1,10,8],[2,10,8],[3,8,8]]]
topn|[[[1,10],[2,10]],[[1],[2],[3]],[[30,3],[10,2]]]
variant-tie|[[[1,1,1],[2,1,2],[3,1,3]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

강좌별 GROUP BY 후 ORDER BY SUM(seats) DESC,class_id LIMIT2 →30 리듬3석,10 나무 모빌2석. score변형 A의 RANK는1/1/1, ROW_NUMBER1/2/3. 기본 데이터 LAST_VALUE는 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING으로A 전체8.

평가 기준: Top2 30/10과좌석3/2, tie-breaker class_id, 변형순위1/1/1과1/2/3, LAST_VALUE 끝범위까지 제출하면 통과.

새 맥락에 적용하기

일자별 대여 기록에 동일시각 두 건을 넣고 ROWS 누적과 RANGE 누적을 비교하라. 고유 대여id와 프레임을 명시해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “GROUP BY는 행을 모으지만 OVER는 각 행을 남긴 채 주변을 계산한다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

나무 경로를 따라 내려가기

해결할 문제와 목표

분류의 자식은 찾았지만 손자와 순환 경로를 놓쳤다.

재귀 CTE와 START WITH·CONNECT BY·PRIOR·LEVEL의 역할 및 SELF JOIN 차이를 설명한다.

먼저 알아둘 것

  1. 순위와 누적은 행을 남긴다의 예측·실행·오류복구 실습

핵심 한 문장: 뿌리에서 시작해 부모 번호와 맞는 자식을 반복해서 붙인다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

폴더 안 폴더를 차례로 연다.

비유가 닿지 않는 곳: 데이터는 진짜 나무라는 보장이 없고 순환·고아·깊이 제한으로 일부가 누락될 수 있다.

실제로 작동하는 원리

기본 category1 만들기→2 목공/3 종이, 별도 뿌리4 음악. anchor는id1을 depth1로 선택한다. 첫 반복은 parent_id=1인2/3을 depth2로 붙이고 다음 자식이 없어 종료한다. 원 교안의0기반 깊이를 Oracle LEVEL과 혼동하지 않게 여기서는 뿌리1로 통일한다. SELF JOIN 한 번은 바로 부모만 조회하고 재귀는 임의 깊이를 확장한다. 새(5,2,조각) 추가 시 손자5는depth3이고 id1의 직접 자식 SELF JOIN에는 안 나온다. Oracle START WITH id=1은 시작행, CONNECT BY PRIOR id=parent_id에서 PRIOR id는 부모 id, LEVEL은1부터인 깊이다. 반대로 PRIOR parent_id=id면 위로 올라간다. Oracle NOCYCLE/CONNECT_BY_ISCYCLE은 순환 방어와 표시를 돕는다. SQLite는 방문 path '/1/2/' 안에 '/id/'가 있는지 검사하여 재방문을 막고 최대깊이10도 둔다. 이것은 순환을 조용히 잘라낼 수 있으므로 데이터 오류 보고와 별개이며, 깊이10에 닿으면 더 깊은 정상노드 누락 가능성도 기록한다. 순환변형 category1.parent_id=2이면1→2→1 순환이지만 id1 anchor와 path 검사 때문에 결과1/2/3에서 멈춘다. parent IS NULL anchor만 쓰면 그 순환 컴포넌트는 출발점 자체가 없어 결과에서 사라진다. Oracle WHERE 필터는 계층 구성 후 개별 행을 제거할 수 있어 자손 전체 가지치기 조건과 같지 않다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

WITH RECURSIVE tree(id,name,depth,path) AS (SELECT id,name,1,'/'||id||'/' FROM category WHERE id=1 UNION ALL SELECT c.id,c.name,t.depth+1,t.path||c.id||'/' FROM category c JOIN tree t ON c.parent_id=t.id WHERE t.depth<10 AND instr(t.path,'/'||c.id||'/')=0) SELECT id,name,depth,path FROM tree ORDER BY depth,id;

예상 결과

[[[1, "만들기", 1, "/1/"], [2, "목공", 2, "/1/2/"], [3, "종이", 2, "/1/3/"]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 음악0, 손자5 depth3, 방문경로 검사, 순환 종료와 잘린 가지 경고를 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. SELF JOIN 한 번은 전체 자손 탐색이 아니다.
  2. Oracle NOCYCLE과 SQLite 경로 검사는 결과의 순환 표시 방식까지 동일하지 않다.

확인 문제

최대깊이10만 있으면 순환 자료도 올바른 트리로 정제된 것인가?

자기평가 · 자동채점 아님

평가 기준: 중단≠정제, 중복 또는 정상자손 누락, 방문경로/원인복구를 설명하면 통과.

해설 보기

아니다. 반복을 한도에서 중단했을 뿐 같은 노드가 여러 번 출력되거나 정상 깊은 자손이 잘릴 수 있다. 방문검사와 오류 보고가 추가로 필요하다.

독립 실습

음악4의 직접 자식 수를 구하라. 목공2 아래 조각5를 추가한 뒤 id1 시작 트리의 깊이를 기록하고 순환1↔2에서 종료시키는 조건을 작성하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 음악0, 손자5 depth3, 방문경로 검사, 순환 종료와 잘린 가지 경고를 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: tree
WITH RECURSIVE tree(id,name,depth,path) AS (SELECT id,name,1,'/'||id||'/' FROM category WHERE id=1 UNION ALL SELECT c.id,c.name,t.depth+1,t.path||c.id||'/' FROM category c JOIN tree t ON c.parent_id=t.id WHERE t.depth<10 AND instr(t.path,'/'||c.id||'/')=0) SELECT id,name,depth,path FROM tree ORDER BY depth,id;
-- case: self-versus-grandchild
INSERT INTO category VALUES(5,2,'조각'); SELECT c.id,p.id FROM category c JOIN category p ON c.parent_id=p.id WHERE p.id=1 ORDER BY c.id; WITH RECURSIVE tree(id,name,depth,path) AS (SELECT id,name,1,'/'||id||'/' FROM category WHERE id=1 UNION ALL SELECT c.id,c.name,t.depth+1,t.path||c.id||'/' FROM category c JOIN tree t ON c.parent_id=t.id WHERE t.depth<10 AND instr(t.path,'/'||c.id||'/')=0) SELECT id,name,depth,path FROM tree ORDER BY depth,id;
-- case: cycle-variant
UPDATE category SET parent_id=2 WHERE id=1; WITH RECURSIVE tree(id,name,depth,path) AS (SELECT id,name,1,'/'||id||'/' FROM category WHERE id=1 UNION ALL SELECT c.id,c.name,t.depth+1,t.path||c.id||'/' FROM category c JOIN tree t ON c.parent_id=t.id WHERE t.depth<10 AND instr(t.path,'/'||c.id||'/')=0) SELECT id,name,depth,path FROM tree ORDER BY depth,id;
-- case: independent-music
SELECT COUNT(*) FROM category WHERE parent_id=4;

SELECT별 고정 정답과 오류 유형

{
  "tree": [
    [
      [
        1,
        "만들기",
        1,
        "/1/"
      ],
      [
        2,
        "목공",
        2,
        "/1/2/"
      ],
      [
        3,
        "종이",
        2,
        "/1/3/"
      ]
    ]
  ],
  "self-versus-grandchild": [
    [
      [
        2,
        1
      ],
      [
        3,
        1
      ]
    ],
    [
      [
        1,
        "만들기",
        1,
        "/1/"
      ],
      [
        2,
        "목공",
        2,
        "/1/2/"
      ],
      [
        3,
        "종이",
        2,
        "/1/3/"
      ],
      [
        5,
        "조각",
        3,
        "/1/2/5/"
      ]
    ]
  ],
  "cycle-variant": [
    [
      [
        1,
        "만들기",
        1,
        "/1/"
      ],
      [
        2,
        "목공",
        2,
        "/1/2/"
      ],
      [
        3,
        "종이",
        2,
        "/1/3/"
      ]
    ]
  ],
  "independent-music": [
    [
      [
        0
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE category(id NUMBER PRIMARY KEY,parent_id NUMBER REFERENCES category(id),name VARCHAR2(30));
INSERT INTO category VALUES(1,NULL,'만들기'); INSERT INTO category VALUES(2,1,'목공'); INSERT INTO category VALUES(3,1,'종이'); INSERT INTO category VALUES(4,NULL,'음악'); INSERT INTO category VALUES(5,2,'조각'); COMMIT;
SELECT id,name,LEVEL depth,SYS_CONNECT_BY_PATH(id,'/') path FROM category START WITH id=1 CONNECT BY PRIOR id=parent_id ORDER SIBLINGS BY id;
SELECT c.id,p.id parent FROM category c JOIN category p ON c.parent_id=p.id WHERE p.id=1 ORDER BY c.id;
UPDATE category SET parent_id=2 WHERE id=1;
SELECT id,LEVEL depth,CONNECT_BY_ISCYCLE cycle_flag FROM category START WITH id=1 CONNECT BY NOCYCLE PRIOR id=parent_id ORDER SIBLINGS BY id;
ROLLBACK;
-- First query hierarchical order 1(depth1),2(2),5(3),3(2), unlike SQLite lesson breadth ordering.
-- Oracle cycle_flag semantics are not reproduced by SQLite visited-path output. NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

tree|[[[1,"만들기",1,"/1/"],[2,"목공",2,"/1/2/"],[3,"종이",2,"/1/3/"]]]
self-versus-grandchild|[[[2,1],[3,1]],[[1,"만들기",1,"/1/"],[2,"목공",2,"/1/2/"],[3,"종이",2,"/1/3/"],[5,"조각",3,"/1/2/5/"]]]
cycle-variant|[[[1,"만들기",1,"/1/"],[2,"목공",2,"/1/2/"],[3,"종이",2,"/1/3/"]]]
independent-music|[[[0]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

음악 자식0. 조각 추가 후1/2/3/5의 깊이는1/2/2/3. 재귀 WHERE instr(t.path,'/'||c.id||'/')=0 AND t.depth<10으로 종료한다. 원인인 잘못된 부모1→2는 데이터 담당자 확인 후 복구하며 깊이제한은 완전성 보장이 아니다.

평가 기준: 음악0, 손자5 depth3, 방문경로 검사, 순환 종료와 잘린 가지 경고를 모두 제출하면 통과.

새 맥락에 적용하기

가상 공구 분류에 고아 parent와 순환을 각각 넣고 발견 쿼리·조회 한계·복구 책임을 기록하라. FK 고아 차단과 순환 검사 차이를 설명해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “뿌리에서 시작해 부모 번호와 맞는 자식을 반복해서 붙인다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

가로 표로 펴고 다시 세로로

해결할 문제와 목표

채널별 보고서를 펴다가 없는 값과0을 혼동하고 원본을 복구할 수 있다고 생각했다.

PIVOT·UNPIVOT을 조건부 집계/UNION ALL과 비교하고 NULL·합계·정보손실을 검증한다.

먼저 알아둘 것

  1. 나무 경로를 따라 내려가기의 예측·실행·오류복구 실습

핵심 한 문장: 피벗은 묶어서 가로로 펴는 것이지 모든 원행을 보존하는 변환이 아니다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

동전 종류별로 칸에 모아 세고 다시 칸 라벨을 붙인다.

비유가 닿지 않는 곳: 모으는 동안 개별 동전의 출처는 사라져 역변환으로 원행을 되살릴 수 없다.

실제로 작동하는 원리

sales 북/온라인2,북/현장3,남/온라인5를 피벗하면 남(5,NULL),북(2,3). SQLite SUM(CASE WHEN channel='온라인' THEN n END)처럼 ELSE0을 생략하면 없는 채널이NULL이라 Oracle PIVOT SUM과 맞는다. 표시용 COALESCE로 남 현장을0으로 만들 수 있지만 미수집/부재와 실제0의 구별을 따로 정해야 한다. Oracle PIVOT은 피벗/집계에 안 쓰인 열로 암묵 그룹화하므로 입력에 불필요한 id를 넣으면 지역별 한 행이 안 된다. 필요한 region,channel,n만 인라인 뷰에서 고른다. UNPIVOT 기본은NULL 제외라3행, INCLUDE NULLS는 남/현장 NULL도 포함해4행. SQLite UNION ALL은 기본적으로NULL도 남기므로 EXCLUDE 대응에는 WHERE n IS NOT NULL을 추가한다. 원자료 합10=피벗의 비NULL 셀합10=UNPIVOT 합10이다. 온라인2를 하나 더 넣으면 북 온라인4로 합쳐지므로 UNPIVOT해도 두 건2+2는 복구되지 않는다. 분류별 완료좌석 가로 표에서는 category2=2,3=1,4=3을 검산한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

SELECT region,SUM(CASE WHEN channel='온라인' THEN n END) online,SUM(CASE WHEN channel='현장' THEN n END) onsite FROM sales GROUP BY region ORDER BY region;

예상 결과

[[["남", 5, null], ["북", 2, 3]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 피벗셀5/NULL/2/3, NULL 포함4행/제외3행, 기본합10·변형합12, 중복원행 정보손실을 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. ELSE0은 부재와0을 합치는 정책 변경이다.
  2. PIVOT 집계와 UNPIVOT은 일반적인 완전 역함수가 아니다.

확인 문제

Oracle 기본 UNPIVOT과 SQLite UNION ALL을 그대로 비교하면 왜 행수가 다른가?

자기평가 · 자동채점 아님

평가 기준: 남/현장NULL 한 행을 지목하고3행/4행 차이 및 WHERE n IS NOT NULL 복구를 쓰면 통과.

해설 보기

Oracle 기본 UNPIVOT은NULL 셀을 제외하지만 UNION ALL은NULL 행도 보존하기 때문이다.

독립 실습

기본 피벗을 만들고 INCLUDE/EXCLUDE NULLS에 대응하는 세로변환3행/4행을 구하라. 중복 북/온라인2를 넣은 뒤 원본4행이 그대로 복구되는지 설명하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 피벗셀5/NULL/2/3, NULL 포함4행/제외3행, 기본합10·변형합12, 중복원행 정보손실을 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE sales(region TEXT,channel TEXT,n INTEGER); INSERT INTO sales VALUES('북','온라인',2),('북','현장',3),('남','온라인',5);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: pivot
SELECT region,SUM(CASE WHEN channel='온라인' THEN n END) online,SUM(CASE WHEN channel='현장' THEN n END) onsite FROM sales GROUP BY region ORDER BY region;
-- case: unpivot-nulls
CREATE VIEW wide AS SELECT region,SUM(CASE WHEN channel='온라인' THEN n END) online,SUM(CASE WHEN channel='현장' THEN n END) onsite FROM sales GROUP BY region; SELECT region,'온라인' channel,online n FROM wide UNION ALL SELECT region,'현장',onsite FROM wide ORDER BY region,channel; SELECT region,channel,n FROM (SELECT region,'온라인' channel,online n FROM wide UNION ALL SELECT region,'현장',onsite FROM wide) WHERE n IS NOT NULL ORDER BY region,channel;
-- case: category-pivot
SELECT SUM(CASE WHEN c.category_id=2 THEN b.seats ELSE 0 END),SUM(CASE WHEN c.category_id=3 THEN b.seats ELSE 0 END),SUM(CASE WHEN c.category_id=4 THEN b.seats ELSE 0 END) FROM booking b JOIN class c ON c.id=b.class_id WHERE b.status='완료';
-- case: variant-duplicate
INSERT INTO sales VALUES('북','온라인',2); SELECT region,SUM(CASE WHEN channel='온라인' THEN n END),SUM(CASE WHEN channel='현장' THEN n END) FROM sales GROUP BY region ORDER BY region; SELECT SUM(n),COUNT(*) FROM sales;

SELECT별 고정 정답과 오류 유형

{
  "pivot": [
    [
      [
        "남",
        5,
        null
      ],
      [
        "북",
        2,
        3
      ]
    ]
  ],
  "unpivot-nulls": [
    [
      [
        "남",
        "온라인",
        5
      ],
      [
        "남",
        "현장",
        null
      ],
      [
        "북",
        "온라인",
        2
      ],
      [
        "북",
        "현장",
        3
      ]
    ],
    [
      [
        "남",
        "온라인",
        5
      ],
      [
        "북",
        "온라인",
        2
      ],
      [
        "북",
        "현장",
        3
      ]
    ]
  ],
  "category-pivot": [
    [
      [
        2,
        1,
        3
      ]
    ]
  ],
  "variant-duplicate": [
    [
      [
        "남",
        5,
        null
      ],
      [
        "북",
        4,
        3
      ]
    ],
    [
      [
        12,
        4
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE sales(region VARCHAR2(10),channel VARCHAR2(20),n NUMBER);
INSERT INTO sales VALUES('북','온라인',2); INSERT INTO sales VALUES('북','현장',3); INSERT INTO sales VALUES('남','온라인',5); COMMIT;
CREATE VIEW wide AS SELECT region,online,onsite FROM (SELECT region,channel,n FROM sales) PIVOT(SUM(n) FOR channel IN ('온라인' AS online,'현장' AS onsite));
SELECT * FROM wide ORDER BY region;
SELECT region,channel,n FROM wide UNPIVOT(n FOR channel IN(online AS '온라인',onsite AS '현장')) ORDER BY region,channel;
SELECT region,channel,n FROM wide UNPIVOT INCLUDE NULLS(n FOR channel IN(online AS '온라인',onsite AS '현장')) ORDER BY region,channel;
-- Expected 2/3/4 rows, totals10. NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

pivot|[[["남",5,null],["북",2,3]]]
unpivot-nulls|[[["남","온라인",5],["남","현장",null],["북","온라인",2],["북","현장",3]],[["남","온라인",5],["북","온라인",2],["북","현장",3]]]
category-pivot|[[[2,1,3]]]
variant-duplicate|[[["남",5,null],["북",4,3]],[[12,4]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

남5/NULL,북2/3. NULL 포함4행·제외3행이다. 북/온라인2 추가 후 북4/3, 전체12; 역변환은 북온라인4 한 행만 남겨 원본2+2 두 행을 복원하지 못한다.

평가 기준: 피벗셀5/NULL/2/3, NULL 포함4행/제외3행, 기본합10·변형합12, 중복원행 정보손실을 모두 제출하면 통과.

새 맥락에 적용하기

요일별 공구대여 가로표를 세로로 바꾸고 무대여0과 미집계NULL의 정책을 적어라. 합계뿐 아니라 행수·누락을 검증해야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “피벗은 묶어서 가로로 펴는 것이지 모든 원행을 보존하는 변환이 아니다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

코드의 모양을 정확히 검사하기

해결할 문제와 목표

A-12를 찾으려다 XA-12와 A-123도 유효 코드로 통과시켰다.

REGEXP_LIKE·REGEXP_SUBSTR·REGEXP_REPLACE를 ASCII 패턴 대조로 검증한다.

먼저 알아둘 것

  1. 가로 표로 펴고 다시 세로로의 예측·실행·오류복구 실습

핵심 한 문장: 부분을 찾는 것과 전체 형식을 검사하는 것은 다른 일이다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

모양을 맞춰 보는 자와 필요한 조각을 잘라내는 가위다.

비유가 닿지 않는 곳: 정규식 통과는 실제 존재하는 코드·권한·사실의 보장이 아니다.

실제로 작동하는 원리

허용 코드 규칙은 대문자A,하이픈,ASCII 숫자 정확히2개, 개행없음이다. ^A-[0-9]{2}$가 단일행 시작/끝을 고정한다. A-12/A-00만 통과하고 A-1X/XA-12/A-123/빈문자/NULL은 제외한다. '$'와 개행 규칙은 엔진마다 경계가 달라질 수 있어 이 과제는 길이4도 함께 검증한다. 대소문자 정책은 명시한다. Oracle REGEXP_LIKE는 조건이라 WHERE 또는 CASE WHEN에 사용한다. REGEXP_SUBSTR(s,'[0-9]+')는 첫 숫자 덩어리, REGEXP_REPLACE(s,'[0-9]','X')는 숫자 전체 치환이다. A-123에서 추출123/치환A-XXX가 나와도 코드 검증 통과라는 뜻은 아니다. 정규식 불일치 추출은NULL, 입력NULL도NULL이다. SQLite 기본엔 이 Oracle 함수들이 없다. runner가 Python re.search/re.sub로 py_match/py_extract/py_replace라는 별도 UDF를 등록하며 실제 문자열 알고리즘을 실행한다. Oracle 엔진의 POSIX·Unicode·NLS·occurrence·match_param 전체를 구현한 호환층이 아니다. SQL 파일도 UDF 이름으로 분리한다. ASCII 예제만 동일 목적 결과를 비교하고 Oracle은 미실행이다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

SELECT id,s FROM codes WHERE length(s)=4 AND py_match('^A-[0-9]{2}$',s)=1 ORDER BY id;

예상 결과

[[[1, "A-12"], [5, "A-00"]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 통과1/5, 잘못된 부분매칭1/3/4/5, 추출123/치환A-XXX, 개행·대소문자·NULL 처리를 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. 외부 사용자 패턴을 제한 없이 실행하지 않는다. 과도한 정규식은 비용·보안 문제가 있다.
  2. SQLite UDF의 Python re 결과를 Oracle 전기능 인증으로 적지 않는다.

확인 문제

REGEXP_SUBSTR가123을 돌려주면 A-123은 유효한 A-두자리 코드인가?

자기평가 · 자동채점 아님

평가 기준: 추출과 검증을 분리하고 ^...$ 및 length4를 설명하면 통과.

해설 보기

아니다. 추출 성공과 전체 형식 검증은 다른 목적이며 길이·경계 조건에서 탈락한다.

독립 실습

유효 코드만 찾고 숫자 추출/치환을 별도 열로 작성하라. XA-12/A-123/개행/a-12가 왜 검증에서는 탈락하는지 설명하라.

남길 증거: 직접 작성한 SQL, 입력→중간행→최종행 표, python3 run.py 출력, 변형 입력 결과 및 오류 원인·복구 기록을 제출한다. 통과1/5, 잘못된 부분매칭1/3/4/5, 추출123/치환A-XXX, 개행·대소문자·NULL 처리를 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE codes(id INTEGER PRIMARY KEY,s TEXT); INSERT INTO codes VALUES(1,'A-12'),(2,'A-1X'),(3,'XA-12'),(4,'A-123'),(5,'A-00'),(6,NULL),(7,'');

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: valid-code
SELECT id,s FROM codes WHERE length(s)=4 AND py_match('^A-[0-9]{2}$',s)=1 ORDER BY id;
-- case: extract-replace
SELECT id,py_extract('[0-9]+',s),py_replace('[0-9]','X',s) FROM codes ORDER BY id;
-- case: variant-newline
INSERT INTO codes VALUES(8,'A-12'||char(10)),(9,'a-12'); SELECT id FROM codes WHERE length(s)=4 AND py_match('^A-[0-9]{2}$',s)=1 ORDER BY id;
-- case: unanchored-error
SELECT id FROM codes WHERE py_match('A-[0-9]{2}',s)=1 ORDER BY id;

SELECT별 고정 정답과 오류 유형

{
  "valid-code": [
    [
      [
        1,
        "A-12"
      ],
      [
        5,
        "A-00"
      ]
    ]
  ],
  "extract-replace": [
    [
      [
        1,
        "12",
        "A-XX"
      ],
      [
        2,
        "1",
        "A-XX"
      ],
      [
        3,
        "12",
        "XA-XX"
      ],
      [
        4,
        "123",
        "A-XXX"
      ],
      [
        5,
        "00",
        "A-XX"
      ],
      [
        6,
        null,
        null
      ],
      [
        7,
        null,
        ""
      ]
    ]
  ],
  "variant-newline": [
    [
      [
        1
      ],
      [
        5
      ]
    ]
  ],
  "unanchored-error": [
    [
      [
        1
      ],
      [
        3
      ],
      [
        4
      ],
      [
        5
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE codes(id NUMBER PRIMARY KEY,s VARCHAR2(20));
INSERT INTO codes VALUES(1,'A-12'); INSERT INTO codes VALUES(2,'A-1X'); INSERT INTO codes VALUES(3,'XA-12'); INSERT INTO codes VALUES(4,'A-123'); INSERT INTO codes VALUES(5,'A-00'); INSERT INTO codes VALUES(6,NULL); INSERT INTO codes VALUES(7,''); COMMIT;
SELECT id,s FROM codes WHERE LENGTH(s)=4 AND REGEXP_LIKE(s,'^A-[0-9]{2}$','c') ORDER BY id;
SELECT id,REGEXP_SUBSTR(s,'[0-9]+',1,1,'c'),REGEXP_REPLACE(s,'[0-9]','X',1,0,'c') FROM codes ORDER BY id;
-- Valid id1,5. id7 empty string treated as NULL in Oracle (SQLite replacement remains empty string). NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

valid-code|[[[1,"A-12"],[5,"A-00"]]]
extract-replace|[[[1,"12","A-XX"],[2,"1","A-XX"],[3,"12","XA-XX"],[4,"123","A-XXX"],[5,"00","A-XX"],[6,null,null],[7,null,""]]]
variant-newline|[[[1],[5]]]
unanchored-error|[[[1],[3],[4],[5]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

length(s)=4 AND py_match('^A-[0-9]{2}$',s)=1 →id1,5. 추출은 A-123에서123, 치환은A-XXX지만 검증에는 탈락. 길이와 경계·대문자 조건을 각각 검사한다. Oracle은 comparison.oracle.sql의 REGEXP_LIKE(...,'c')를 읽어 대조한다.

평가 기준: 통과1/5, 잘못된 부분매칭1/3/4/5, 추출123/치환A-XXX, 개행·대소문자·NULL 처리를 모두 제출하면 통과.

새 맥락에 적용하기

합성 장비코드 B-세자리 규칙으로 옮겨 정상·과매칭·개행·NULL 테스트를 만들라. 최소5개 테스트와 정확한 허용조건이 있어야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “부분을 찾는 것과 전체 형식을 검사하는 것은 다른 일이다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

바꾸기 전에 되돌릴 길 만들기

해결할 문제와 목표

WHERE를 빼고 모든 접수를 완료로 바꾸거나 재시도로 같은 행을 두 번 넣었다.

INSERT·UPDATE·DELETE·UPSERT와 Oracle MERGE를 비교하고 COMMIT·ROLLBACK·SAVEPOINT를 검증한다.

먼저 알아둘 것

  1. 코드의 모양을 정확히 검사하기의 예측·실행·오류복구 실습

핵심 한 문장: 바꾸기 전에 대상을 세고 확정 전에 결과를 대조한다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

연필로 고친 뒤 확인하고 잉크로 확정한다.

비유가 닿지 않는 곳: COMMIT 뒤 ROLLBACK은 지우개가 아니며 자동커밋과 DDL은 별도 규칙이다.

실제로 작동하는 원리

DML CRUD: INSERT 새 행, SELECT 읽기, UPDATE 조건 행 수정, DELETE 조건 행 삭제다. 원자성은 한 거래를 전부/전무로, 일관성은 선언한 제약과 업무규칙을 유지, 격리성은 동시 거래 간 간섭을 통제, 지속성은 확정된 기록 보존이다. 일관성을 DB가 미정의한 업무규칙까지 알아서 지켜준다는 뜻은 아니다. 메모리 DB는 프로세스 종료 후 자료를 잃으므로 이번 COMMIT 검산은 지속성의 디스크 장애 보장을 시험하지 않는다. 단일 연결 예제는 동시 운영 격리성 검증도 아니다. runner의 sqlite3.connect(...,isolation_level=None)은 암묵 BEGIN을 끄고 SQLite 자동커밋을 쓰며 직접 BEGIN으로 거래를 시작한다. seed 적재 뒤 명시 BEGIN→SAVEPOINT edit→UPDATE102 완료→SELECT 확인→ROLLBACK TO edit→RELEASE edit→COMMIT으로102 취소를 복구한다. ROLLBACK TO는 저장점 이후만 취소하고 거래는 남는다. 전체 ROLLBACK은 BEGIN 이후 모두 취소한다. COMMIT은 저장점을 끝내고 변경을 확정한다. 자동커밋 상태에서 UPDATE 후 ROLLBACK은 활성 거래가 없어 실패하며 값은 이미 바뀌었다. Oracle에는 SQLite의 BEGIN 거래시작 문법을 그대로 쓰지 않는다(BEGIN은 PL/SQL 블록 문맥). 첫 DML로 거래가 시작되고 SAVEPOINT/ROLLBACK TO/COMMIT을 쓴다. Oracle MERGE는 USING 원본과 ON 매칭 후 MATCHED UPDATE / NOT MATCHED INSERT를 한 문장에 명시한다. SQLite INSERT...ON CONFLICT(id) DO UPDATE는 UNIQUE/PK 충돌을 기준으로 동작하여 임의 ON 조건을 가진 MERGE 전체와 동등하지 않다. 예제 stock id10=2에 source10=3,20=4를 넣어 결과10=3/20=4로 비교한다. source의 매칭키를 유일하게 보장해야 Oracle 같은 target을 여러 번 갱신하는 위험을 피한다. UPDATE 사전 SELECT만으로 동시성 안전을 보장하지 않는다. 같은 트랜잭션에서 건수·업무조건 검증과 잠금/격리 정책을 함께 설계해야 한다. 운영 연결에는 이 자료를 실행하지 않는다. 추가 isolation.py는 실제 두 연결을 같은 이름의 메모리 DB에 연결한다. read_uncommitted=0에서 미확정 읽기는 SQLITE_LOCKED로 막히며 롤백 후2, 커밋 후3을 읽는다. SQLite 공유 캐시 잠금의 예이며 모든 DB의 동시성·격리수준을 재현하지 않는다. 마지막 연결을 닫으면 자료가 사라져 메모리 실습의 지속성 한계를 확인한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.
  6. python3 isolation.py를 별도로 실행해 isolation-expected.txt와 비교한다. A의 미확정 쓰기 동안 B의 읽기는 공유 캐시 잠금으로 차단되고, ROLLBACK 뒤2·COMMIT 뒤3을 본다. 모든 연결을 닫으면 메모리 데이터가 사라진다. 디스크 지속성과 Oracle 격리 동작은 검증하지 않는다.

입력 예제

BEGIN; SAVEPOINT edit; UPDATE booking SET status='완료' WHERE id=102; SELECT id,status FROM booking WHERE id=102; ROLLBACK TO edit; RELEASE edit; SELECT id,status FROM booking WHERE id=102; COMMIT;

예상 결과

[[[102, "완료"]], [[102, "취소"]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 예상 1행/실제 5행/102 취소 복구, 병합 결과 두 행, 자동커밋에서 7 유지와 오류 이유, SQLite·Oracle 방언 차이를 모두 제출하면 통과.

흔한 오해와 실패 복구

  1. 오류가 났다고 앞선 모든 문장이 자동 롤백된다고 가정하지 말고 명시 ROLLBACK을 수행한다.
  2. 외부 운영 DB·실권한·유료 엔진은 사용하지 않는다.

확인 문제

ROLLBACK TO s와 ROLLBACK 전체는 어떤 변경을 취소하며 COMMIT 후도 같은가? 취소 후 바깥 거래가 유지되는지 종료되는지도 비교하라.

자기평가 · 자동채점 아님

평가 기준: 취소범위 둘, 거래 유지/종료 차이, COMMIT 뒤 복구 불가 세 가지를 설명하면 통과.

해설 보기

전자는 저장점 뒤 변경만, 후자는 현재 거래 전체를 취소한다. COMMIT한 이전 거래는 둘로 되돌릴 수 없다.

독립 실습

WHERE 없는 UPDATE를 사전 예상1행/실제5행으로 검출하고 저장점으로 취소하라. stock에10=3/20=4를 병합하고, 자동커밋 갱신은 ROLLBACK으로 복구되지 않는 반례를 설명하라.

남길 증거: 직접 작성한 SQL, 중간행, 실행 출력, 입력변형과 오류복구 기록. 예상 1행/실제 5행/102 취소 복구, 병합 결과 두 행, 자동커밋에서 7 유지와 오류 이유, SQLite·Oracle 방언 차이를 모두 제출하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE stock(id INTEGER PRIMARY KEY,n INTEGER NOT NULL CHECK(n>=0)); INSERT INTO stock VALUES(10,2);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: savepoint
BEGIN; SAVEPOINT edit; UPDATE booking SET status='완료' WHERE id=102; SELECT id,status FROM booking WHERE id=102; ROLLBACK TO edit; RELEASE edit; SELECT id,status FROM booking WHERE id=102; COMMIT;
-- case: crud-rollback
BEGIN; INSERT INTO booking VALUES(106,40,'가상',1,'완료',5000); UPDATE booking SET seats=2 WHERE id=106; SELECT id,seats FROM booking WHERE id=106; DELETE FROM booking WHERE id=106; SELECT COUNT(*) FROM booking; ROLLBACK; SELECT COUNT(*) FROM booking;
-- case: missing-where-recovery
BEGIN; SELECT COUNT(*) FROM booking WHERE id=102; SAVEPOINT s; UPDATE booking SET status='완료'; SELECT changes(); ROLLBACK TO s; RELEASE s; SELECT id,status FROM booking WHERE id=102; COMMIT;
-- case: upsert
INSERT INTO stock VALUES(10,3),(20,4) ON CONFLICT(id) DO UPDATE SET n=excluded.n; SELECT id,n FROM stock ORDER BY id;
-- case: variant-autocommit
UPDATE stock SET n=7 WHERE id=10; ROLLBACK; SELECT n FROM stock WHERE id=10;
-- case: atomic-rollback
BEGIN; UPDATE stock SET n=9 WHERE id=10; INSERT INTO stock VALUES(20,-1); ROLLBACK; SELECT id,n FROM stock ORDER BY id;

SELECT별 고정 정답과 오류 유형

{
  "savepoint": [
    [
      [
        102,
        "완료"
      ]
    ],
    [
      [
        102,
        "취소"
      ]
    ]
  ],
  "crud-rollback": [
    [
      [
        106,
        2
      ]
    ],
    [
      [
        5
      ]
    ],
    [
      [
        5
      ]
    ]
  ],
  "missing-where-recovery": [
    [
      [
        1
      ]
    ],
    [
      [
        5
      ]
    ],
    [
      [
        102,
        "취소"
      ]
    ]
  ],
  "upsert": [
    [
      [
        10,
        3
      ],
      [
        20,
        4
      ]
    ]
  ],
  "variant-autocommit": [
    "ERROR OperationalError",
    [
      [
        7
      ]
    ]
  ],
  "atomic-rollback": [
    "ERROR IntegrityError",
    [
      [
        10,
        2
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE stock(id NUMBER PRIMARY KEY,n NUMBER NOT NULL CHECK(n>=0));
CREATE TABLE src(id NUMBER PRIMARY KEY,n NUMBER NOT NULL CHECK(n>=0));
INSERT INTO stock VALUES(10,2); INSERT INTO src VALUES(10,3); INSERT INTO src VALUES(20,4); COMMIT;
SAVEPOINT before_merge;
MERGE INTO stock t USING src s ON(t.id=s.id)
WHEN MATCHED THEN UPDATE SET t.n=s.n
WHEN NOT MATCHED THEN INSERT(id,n) VALUES(s.id,s.n);
SELECT id,n FROM stock ORDER BY id;
ROLLBACK TO before_merge;
SELECT id,n FROM stock ORDER BY id;
UPDATE stock SET n=n+1 WHERE id=10;
SAVEPOINT edit;
UPDATE stock SET n=99 WHERE id=10;
ROLLBACK TO edit;
COMMIT;
SELECT id,n FROM stock ORDER BY id;
-- Expected: (10,3),(20,4); rollback ->(10,2); final commit ->(10,3). NOT RUN.
-- Only hypothetical disposable training schema. No real administrative execution.

SQLite 두 연결의 공유 메모리 격리·확정 가시성 검산

from pathlib import Path
import sqlite3
# Two connections share ONLY this named in-memory database, not a disk file.
uri='file:classroom_isolation?mode=memory&cache=shared'
a=sqlite3.connect(uri,uri=True,isolation_level=None)
b=sqlite3.connect(uri,uri=True,isolation_level=None)
a.executescript((Path(__file__).parent/'setup.sqlite.sql').read_text(encoding='utf-8'))
b.execute('PRAGMA read_uncommitted=0')
assert b.execute('SELECT n FROM stock WHERE id=10').fetchone()==(2,)
print('before=2')
a.execute('BEGIN IMMEDIATE')
a.execute('UPDATE stock SET n=3 WHERE id=10')
try:
    b.execute('SELECT n FROM stock WHERE id=10').fetchone()
except sqlite3.OperationalError as e:
    assert e.sqlite_errorcode & 255 == sqlite3.SQLITE_LOCKED
    print('uncommitted-read=LOCKED')
else:
    raise AssertionError('unexpected dirty read or unsupported isolation test')
a.execute('ROLLBACK')
assert b.execute('SELECT n FROM stock WHERE id=10').fetchone()==(2,)
print('after-rollback=2')
a.execute('BEGIN IMMEDIATE')
a.execute('UPDATE stock SET n=3 WHERE id=10')
a.execute('COMMIT')
assert b.execute('SELECT n FROM stock WHERE id=10').fetchone()==(3,)
print('after-commit=3')
a.close();b.close()
c=sqlite3.connect(uri,uri=True,isolation_level=None)
assert c.execute("SELECT COUNT(*) FROM sqlite_master WHERE name='stock'").fetchone()==(0,)
print('all-connections-closed=memory-data-gone')
c.close()
print('PASS isolation demo; shared-memory locking only; disk durability NOT TESTED')

격리 실습 고정 예상 stdout

before=2
uncommitted-read=LOCKED
after-rollback=2
after-commit=3
all-connections-closed=memory-data-gone
PASS isolation demo; shared-memory locking only; disk durability NOT TESTED

실제 검산한 Python stdout (SQLite; Oracle 제외)

savepoint|[[[102,"완료"]],[[102,"취소"]]]
crud-rollback|[[[106,2]],[[5]],[[5]]]
missing-where-recovery|[[[1]],[[5]],[[102,"취소"]]]
upsert|[[[10,3],[20,4]]]
variant-autocommit|["ERROR OperationalError",[[7]]]
atomic-rollback|["ERROR IntegrityError",[[10,2]]]
PASS 6 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

동일 WHERE의 SELECT로 예상 1행을 기록하고 SAVEPOINT 뒤 UPDATE의 changes()가 5인지 확인한다. 불일치하므로 ROLLBACK TO로 취소하며 102는 취소 상태로 유지된다. SQLite ON CONFLICT 결과는 (10,3),(20,4)다. 자동커밋 UPDATE 뒤 ROLLBACK은 활성 거래가 없어 실패하고 7이 유지된다. 이미 확정된 변경은 새 보상 거래나 백업 복구가 필요하다.

평가 기준: 예상 1행/실제 5행/102 취소 복구, 병합 결과 두 행, 자동커밋에서 7 유지와 오류 이유, SQLite·Oracle 방언 차이를 모두 제출하면 통과.

새 맥락에 적용하기

합성 재고 입출고 거래에서 두 변경 중 두 번째가 CHECK 위반일 때 전체를 취소하라. 첫 변경도 원복되고 행수가 유지되어야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “바꾸기 전에 대상을 세고 확정 전에 결과를 대조한다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

행을 비우는 것과 서랍을 없애는 것

해결할 문제와 목표

TRUNCATE를 DELETE처럼 되돌릴 수 있다고 믿거나 열 삭제로 참조를 깨뜨렸다.

CREATE·ALTER·DROP·TRUNCATE·RENAME·제약조건과 DB별 DDL 거래 경계를 구분한다.

먼저 알아둘 것

  1. 바꾸기 전에 되돌릴 길 만들기의 예측·실행·오류복구 실습

핵심 한 문장: DELETE는 내용, DROP은 구조까지 없애며 TRUNCATE의 거래 규칙은 엔진을 확인한다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

서랍 내용 비우기와 서랍 철거는 다르다.

비유가 닿지 않는 곳: 실제 DB의 암묵커밋·참조·트리거·저장공간 특성까지 비유로 결정할 수 없다.

실제로 작동하는 원리

CREATE TABLE은 열과 PK/UNIQUE/NOT NULL/CHECK/FK 계약을 만든다. PK는 대표 유일 식별, UNIQUE는 보조 식별, NOT NULL은 필수값, CHECK는 행의 허용조건, FK는 부모 참조다. SQLite 일반 표의 일부 PK 선언은 NULL 관련 예외가 있으므로 여기 키는 INTEGER PRIMARY KEY, 복합키 실무 설계에서는 NOT NULL도 명시한다. CHECK(qty>=0)만으로NULL을 금지하지 못한다(UNKNOWN은 실패가 아님); NOT NULL을 함께 쓴다. warehouse의10/A/2를 만들고 ALTER TABLE ADD COLUMN note TEXT DEFAULT 'new', ALTER TABLE RENAME COLUMN qty TO amount, ALTER TABLE RENAME TO warehouse_new로 구조를 바꾼다. SQLite RENAME은 ALTER TABLE 방식이고 Oracle은 RENAME old TO new 구문도 있다. 새 이름 이후 옛 이름 조회는 오류이므로 뷰·코드 참조까지 이관해야 한다. DELETE FROM ... WHERE id=10은 특정 행 DML, WHERE없으면 전행이지만 표는 남는다. Oracle TRUNCATE TABLE은WHERE 없이 전행 비우는 DDL, 일반표는ROLLBACK 불가이며 DELETE 트리거처럼 행별 삭제 절차가 아니다. DROP TABLE은객체 제거로 이후 SELECT 자체 실패. SQLite엔 TRUNCATE가 없어서 DELETE만 목적 대안으로 제공하며 저장공간/트리거/커밋 성질의 등가라고 부르지 않는다. SQLite 명시 BEGIN 안 CREATE/ALTER/DROP은 거래 롤백할 수 있다. Oracle은 문법적으로 유효한 DDL 직전(실행 오류여도) 암묵 COMMIT, 성공한 DDL 직후 COMMIT이 발생한다. 예: DML UPDATE 후 CREATE TABLE을 실행하고 ROLLBACK해도 UPDATE는 이미확정될 수 있다. DDL을 DML복구용 저장점 사이에 넣지 않는다. 이 교재는 메모리에서 SQLite만 실행하고 Oracle 관리문장은 독립 가상 setup의 읽기·순서추론용이다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

ALTER TABLE warehouse ADD COLUMN note TEXT DEFAULT 'new'; ALTER TABLE warehouse RENAME COLUMN qty TO amount; ALTER TABLE warehouse RENAME TO warehouse_new; SELECT id,code,amount,note FROM warehouse_new; SELECT * FROM warehouse;

예상 결과

[[[10, "A", 2, "new"]], "ERROR OperationalError"] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. ALTER 결과, 다섯 제약 위반의 원인, CHECK(NULL) 허용과 NOT NULL 필요성, DELETE/DROP 복구 결과, Oracle DDL 암묵커밋을 모두 설명하면 통과.

흔한 오해와 실패 복구

  1. SQLite executescript는 Python 연결 설정에 따라 거래경계에 영향을 줄 수 있어 runner는 seed 이후 문장별 execute를 사용한다.
  2. DDL 오류라고 직전 Oracle DML이 미확정이라고 가정하지 않는다.

확인 문제

DELETE·TRUNCATE·DROP은 모두 같은가? SQLite DROP의 롤백을 Oracle에도 일반화할 수 있는가?

자기평가 · 자동채점 아님

평가 기준: 세 구문의 대상, WHERE 가능 여부, Oracle DDL 암묵 COMMIT 및 일반표 TRUNCATE 롤백 불가를 설명하면 통과.

해설 보기

행 삭제, 전행 절단, 객체 제거로 대상과 거래 규칙이 다르다. SQLite 명시 거래의 DDL 롤백을 Oracle 일반 DDL에 일반화하면 안 된다.

독립 실습

warehouse를 새 열·열이름·표이름으로 변경하라. 다섯 제약 위반을 구별하고 DELETE/DROP의 롤백 결과와 Oracle TRUNCATE 차이를 표로 제출하라.

남길 증거: 직접 작성한 SQL, 중간행, 실행 출력, 입력변형과 오류복구 기록. ALTER 결과, 다섯 제약 위반의 원인, CHECK(NULL) 허용과 NOT NULL 필요성, DELETE/DROP 복구 결과, Oracle DDL 암묵커밋을 모두 설명하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE warehouse(id INTEGER PRIMARY KEY,code TEXT NOT NULL UNIQUE,qty INTEGER NOT NULL CHECK(qty>=0)); INSERT INTO warehouse VALUES(10,'A',2);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: alter-rename
ALTER TABLE warehouse ADD COLUMN note TEXT DEFAULT 'new'; ALTER TABLE warehouse RENAME COLUMN qty TO amount; ALTER TABLE warehouse RENAME TO warehouse_new; SELECT id,code,amount,note FROM warehouse_new; SELECT * FROM warehouse;
-- case: constraints
INSERT INTO warehouse VALUES(10,'B',1); INSERT INTO warehouse VALUES(20,'A',1); INSERT INTO warehouse VALUES(20,NULL,1); INSERT INTO warehouse VALUES(20,'B',-1); INSERT INTO booking VALUES(106,999,'가상',1,'완료',0); SELECT COUNT(*) FROM warehouse; SELECT COUNT(*) FROM booking;
-- case: check-null-variant
CREATE TABLE weak(n INTEGER CHECK(n>=0)); INSERT INTO weak VALUES(NULL); SELECT n FROM weak; INSERT INTO warehouse VALUES(20,'B',NULL); SELECT COUNT(*) FROM warehouse;
-- case: delete-drop
BEGIN; DELETE FROM warehouse; SELECT COUNT(*) FROM warehouse; ROLLBACK; SELECT COUNT(*) FROM warehouse; BEGIN; DROP TABLE warehouse; ROLLBACK; SELECT COUNT(*) FROM warehouse; DROP TABLE warehouse; SELECT * FROM warehouse;
-- case: ddl-rollback
BEGIN; CREATE TABLE transient(id INTEGER PRIMARY KEY); INSERT INTO transient VALUES(1); ROLLBACK; SELECT name FROM sqlite_master WHERE name='transient';

SELECT별 고정 정답과 오류 유형

{
  "alter-rename": [
    [
      [
        10,
        "A",
        2,
        "new"
      ]
    ],
    "ERROR OperationalError"
  ],
  "constraints": [
    "ERROR IntegrityError",
    "ERROR IntegrityError",
    "ERROR IntegrityError",
    "ERROR IntegrityError",
    "ERROR IntegrityError",
    [
      [
        1
      ]
    ],
    [
      [
        5
      ]
    ]
  ],
  "check-null-variant": [
    [
      [
        null
      ]
    ],
    "ERROR IntegrityError",
    [
      [
        1
      ]
    ]
  ],
  "delete-drop": [
    [
      [
        0
      ]
    ],
    [
      [
        1
      ]
    ],
    [
      [
        1
      ]
    ],
    "ERROR OperationalError"
  ],
  "ddl-rollback": [
    []
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
CREATE TABLE warehouse(id NUMBER PRIMARY KEY,code VARCHAR2(10) NOT NULL UNIQUE,qty NUMBER NOT NULL CHECK(qty>=0));
INSERT INTO warehouse VALUES(10,'A',2); COMMIT;
ALTER TABLE warehouse ADD(note VARCHAR2(20) DEFAULT 'new');
ALTER TABLE warehouse RENAME COLUMN qty TO amount;
RENAME warehouse TO warehouse_new;
SELECT id,code,amount,note FROM warehouse_new;
DELETE FROM warehouse_new WHERE id=10;
ROLLBACK;
SELECT COUNT(*) FROM warehouse_new;
TRUNCATE TABLE warehouse_new;
ROLLBACK;
SELECT COUNT(*) FROM warehouse_new;
DROP TABLE warehouse_new;
-- Reasoned counts after DELETE rollback=1, after TRUNCATE rollback=0; DROP removes object.
-- Oracle DDL implicit commits mean ROLLBACK here does not undo TRUNCATE. NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

alter-rename|[[[10,"A",2,"new"]],"ERROR OperationalError"]
constraints|["ERROR IntegrityError","ERROR IntegrityError","ERROR IntegrityError","ERROR IntegrityError","ERROR IntegrityError",[[1]],[[5]]]
check-null-variant|[[[null]],"ERROR IntegrityError",[[1]]]
delete-drop|[[[0]],[[1]],[[1]],"ERROR OperationalError"]
ddl-rollback|[[]]
PASS 5 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

ALTER 후 결과는 (10,A,2,new)이고 옛 표 조회는 오류다. 제약 오류 순서는 PK 중복, UNIQUE 중복, NOT NULL, CHECK, FK 위반이다. DELETE 후 0행→ROLLBACK 후 1행; 명시 거래의 DROP도 ROLLBACK 후 1행이다. 거래 밖 DROP 뒤에는 조회가 실패한다. Oracle 일반표 TRUNCATE는 전행 비우기 DDL이며 롤백할 수 없고 SQLite에 동일 구문은 없다.

평가 기준: ALTER 결과, 다섯 제약 위반의 원인, CHECK(NULL) 허용과 NOT NULL 필요성, DELETE/DROP 복구 결과, Oracle DDL 암묵커밋을 모두 설명하면 통과.

새 맥락에 적용하기

가상 도서 대출 표의 열 이름 변경 이관안을 작성하라. 새/옛 쿼리 시험과 백업·거래 엔진별 주의가 있어야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “DELETE는 내용, DROP은 구조까지 없애며 TRUNCATE의 거래 규칙은 엔진을 확인한다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

열쇠를 빌려주고 회수하기

해결할 문제와 목표

보고 담당자가 조회만 해야 하는데 수정 권한까지 받았다.

ROLE·GRANT·REVOKE와 직접/역할 권한 경로를 모의 표로 검증한다.

먼저 알아둘 것

  1. 행을 비우는 것과 서랍을 없애는 것의 예측·실행·오류복구 실습

핵심 한 문장: 역할은 권한 묶음이고 하나를 회수해도 다른 허용 경로가 남을 수 있다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

서랍 열쇠 묶음을 역할별로 나눠 준다.

비유가 닿지 않는 곳: SQLite 모의 표가 실제 DB 접근을 차단하지 않으며 역할 상속·소유권·관리자 우회는 별도다.

실제로 작동하는 원리

Oracle CREATE ROLE report_reader는권한을 담는 역할을 만들고 GRANT SELECT ON report_demo TO report_reader는객체 조회권한을 부여한다. GRANT report_reader TO trainee_role은다른 역할에 묶음을 넘기는 예다. REVOKE SELECT ON report_demo FROM report_reader는그 경로만 끊으며 다른 직접 권한을 자동 제거하지 않는다. 객체 권한(SELECT/UPDATE)과 역할을 만들 수 있는 시스템권한(CREATE ROLE)은 다르다. WITH GRANT OPTION 등 재위임 권한은 이 최소권한 예제에 주지 않는다. SQLite는 GRANT/REVOKE/ROLE SQL을 지원하지 않는다. 실습은 roles/membership/permissions라는 일반 표에 허용관계를 기록하고 EXISTS로 판정하는 모의 정책이다. 모의 사용자 trainee가 report_reader 역할을 갖고 그 역할에SELECT만 있으면SELECT1,UPDATE0. 직접 trainee SELECT도 추가한 뒤 역할의SELECT를 회수하면 여전히SELECT1. 직접 경로까지 삭제해야0이다. runner가 실제 사용자 인증·권한을 집행하는 것은 아니며 표를 읽고 바꾸는 Python 프로세스는 여전히 DB접근이 가능하다. 실제 SQLite는 OS파일권한/애플리케이션에서제어한다. 운영 계정·기존 역할·실권한은 절대 바꾸지 않는다. Oracle 파일은 독립 가상 객체와 역할로 구성한 공식구문 대조용이며 실제 실행하지 않는다. 생성 권한이 이미 있는 폐기용 DB라는 가정 없이 붙여넣지 않는다. 이번 실습에 새 계정·설치·인증은 필요 없다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. comparison.oracle.sql은 SQLite runner가 읽지 않는다. 독립 가상 setup을 포함한 Oracle 19c 구문 학습자료이며 실 Oracle 미실행이다. 계정·설치·유료 연결을 새로 만들지 않는다. 관리·권한은 모의 표에서만 실행한다.

입력 예제

SELECT 'SELECT',EXISTS(SELECT 1 FROM permissions p JOIN membership m ON m.role=p.principal WHERE m.person='trainee' AND p.privilege='SELECT' AND p.object_name='report_demo') UNION ALL SELECT 'UPDATE',EXISTS(SELECT 1 FROM permissions p JOIN membership m ON m.role=p.principal WHERE m.person='trainee' AND p.privilege='UPDATE' AND p.object_name='report_demo');

예상 결과

[[["SELECT", 1], ["UPDATE", 0]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. 기본 허용 1/0, 다른 경로가 남은 1과 전체 회수 후 0, 구성원 삭제와 역할 권한의 차이, 실제 권한 미변경을 모두 설명하면 통과.

흔한 오해와 실패 복구

  1. 이 실습은 보안제품이나 실제 DB DCL 집행기가 아니다.
  2. 관리역할·PUBLIC·기존사용자에 권한을 주는 실험은 하지 않는다.

확인 문제

REVOKE 한 번으로 사용자가 반드시 접근할 수 없다고 단정해도 되는가?

자기평가 · 자동채점 아님

평가 기준: 직접 경로가 남은1과 모든 경로 제거 후0을 제시하고 실제 권한 검증과 모의 정책표를 구별하면 통과.

해설 보기

안 된다. 직접 권한과 다른 역할 등 허용 경로를 확인해야 한다. 모의 사례에서는 직접 SELECT가 남아 허용1이다.

독립 실습

trainee에게 SELECT만 허용하는 모의 역할을 검사하라. 직접SELECT가 추가된 상태에서 역할SELECT만 회수한 결과와 모든 경로 회수 결과를 비교하라.

남길 증거: 직접 작성한 SQL, 중간행, 실행 출력, 입력변형과 오류복구 기록. 기본 허용 1/0, 다른 경로가 남은 1과 전체 회수 후 0, 구성원 삭제와 역할 권한의 차이, 실제 권한 미변경을 모두 설명하면 통과.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

CREATE TABLE roles(name TEXT PRIMARY KEY); CREATE TABLE membership(person TEXT,role TEXT REFERENCES roles(name),PRIMARY KEY(person,role)); CREATE TABLE permissions(principal TEXT,privilege TEXT,object_name TEXT,PRIMARY KEY(principal,privilege,object_name)); INSERT INTO roles VALUES('report_reader'); INSERT INTO membership VALUES('trainee','report_reader'); INSERT INTO permissions VALUES('report_reader','SELECT','report_demo');

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: role-policy
SELECT 'SELECT',EXISTS(SELECT 1 FROM permissions p JOIN membership m ON m.role=p.principal WHERE m.person='trainee' AND p.privilege='SELECT' AND p.object_name='report_demo') UNION ALL SELECT 'UPDATE',EXISTS(SELECT 1 FROM permissions p JOIN membership m ON m.role=p.principal WHERE m.person='trainee' AND p.privilege='UPDATE' AND p.object_name='report_demo');
-- case: revoke-other-path
INSERT INTO permissions VALUES('trainee','SELECT','report_demo'); DELETE FROM permissions WHERE principal='report_reader' AND privilege='SELECT'; SELECT EXISTS(SELECT 1 FROM permissions p WHERE p.object_name='report_demo' AND p.privilege='SELECT' AND (p.principal='trainee' OR p.principal IN(SELECT role FROM membership WHERE person='trainee'))); DELETE FROM permissions WHERE principal='trainee'; SELECT EXISTS(SELECT 1 FROM permissions p WHERE p.object_name='report_demo' AND p.privilege='SELECT' AND (p.principal='trainee' OR p.principal IN(SELECT role FROM membership WHERE person='trainee')));
-- case: variant-membership
DELETE FROM membership WHERE person='trainee'; SELECT EXISTS(SELECT 1 FROM permissions p JOIN membership m ON m.role=p.principal WHERE m.person='trainee' AND p.privilege='SELECT'); SELECT COUNT(*) FROM permissions;

SELECT별 고정 정답과 오류 유형

{
  "role-policy": [
    [
      [
        "SELECT",
        1
      ],
      [
        "UPDATE",
        0
      ]
    ]
  ],
  "revoke-other-path": [
    [
      [
        1
      ]
    ],
    [
      [
        0
      ]
    ]
  ],
  "variant-membership": [
    [
      [
        0
      ]
    ],
    [
      [
        1
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

Oracle 19c 전체 setup·공식구문 대조용(실 Oracle 미실행; 운영 DB 금지)

-- Oracle 19c ONLY. NOT EXECUTED. New disposable training schema only.
-- Each file assumes its own empty schema; no real data or privileges.
-- Hypothetical disposable Oracle schema with CREATE ROLE already authorized.
-- No users/passwords are created. Never run against existing roles or production.
CREATE TABLE report_demo(id NUMBER PRIMARY KEY,n NUMBER);
INSERT INTO report_demo VALUES(1,2); COMMIT;
CREATE ROLE report_reader;
CREATE ROLE trainee_role;
GRANT SELECT ON report_demo TO report_reader;
GRANT report_reader TO trainee_role;
GRANT SELECT ON report_demo TO trainee_role;
SELECT role,owner,table_name,privilege FROM role_tab_privs WHERE role IN ('REPORT_READER','TRAINEE_ROLE') ORDER BY role,table_name,privilege;
REVOKE SELECT ON report_demo FROM report_reader;
SELECT role,owner,table_name,privilege FROM role_tab_privs WHERE role IN ('REPORT_READER','TRAINEE_ROLE') ORDER BY role,table_name,privilege;
REVOKE SELECT ON report_demo FROM trainee_role;
REVOKE report_reader FROM trainee_role;
DROP ROLE trainee_role;
DROP ROLE report_reader;
DROP TABLE report_demo;
-- Reader-path removed but direct grant to TRAINEE_ROLE remains until second REVOKE.
-- Official syntax illustration only. Real Oracle role creation/grants NOT RUN.

실제 검산한 Python stdout (SQLite; Oracle 제외)

role-policy|[[["SELECT",1],["UPDATE",0]]]
revoke-other-path|[[[1]],[[0]]]
variant-membership|[[[0]],[[1]]]
PASS 3 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')
실습 해설 보기

기본 정책은 SELECT=1, UPDATE=0이다. 직접 SELECT 경로를 추가한 뒤 역할의 SELECT만 회수하면 허용은 여전히 1이다. 직접 권한까지 삭제하면 0이다. 역할 구성원만 삭제하면 trainee의 허용은 0이지만 역할 권한 자체는 1행 남는다. 실제 DB 권한이 아니라 모의 정책표를 검산한 결과다.

평가 기준: 기본 허용 1/0, 다른 경로가 남은 1과 전체 회수 후 0, 구성원 삭제와 역할 권한의 차이, 실제 권한 미변경을 모두 설명하면 통과.

새 맥락에 적용하기

가상 검수자에게 읽기,담당자에게 갱신을 나누고 담당자 역할회수 후 직접권한이 남는지 정책표로 검사하라. 최소권한과모의한계를 밝혀야 통과.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “역할은 권한 묶음이고 하나를 회수해도 다른 허용 경로가 남을 수 있다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

학습 단원

처음 본 표에도 답하기 — 자체 진단1

해결할 문제와 목표

예제는 따라했지만 새 강좌 추가와 NULL·취소가 섞이면 정원 판단을 잘못한다.

모델링·NULL·함수·조인행수·집계를 작은 독립 통합과제로 진단하고 오답노트를 쓴다.

먼저 알아둘 것

  1. 열쇠를 빌려주고 회수하기의 예측·실행·오류복구 실습

핵심 한 문장: 키→중간행→조건→합계→반례 순서로 새 문제를 검산한다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

장부를 읽는 시험이 아니라 장부를 대조하는 실기다.

비유가 닿지 않는 곳: 이 자체4과제는 공식50문항 모의시험이나 합격판정이 아니다.

실제로 작동하는 원리

해설을 가리고 자체 진단1의 네 과제를 수행한다. A. 신규 class50(category2, 가죽 장식, capacity2), booking106(완료 2석/9000원),107(취소 1석/9000원)을 추가하고 완료 좌석과 정원을 보고한다. B. 기본 class20의 기존 완료 1석에 신규 완료 4석을 더하면 정원4를 넘는지, 쓰기 전에 판정한다. C. NULL paid의 0 표시와 원본 NULL 건수를 동시에 보고한다. D. 접수 없는40을 남기고 JOIN 후 행수와 COUNT(b.id)를 설명한다. A의 중간행은106→2,107→0이므로 used2/capacity2다. 취소까지 합하면3으로 정원초과라는 오답을 만든다. B는1+4=5>4라 거절한다. 이전 교안의 별도 미니 사례 정원2와 혼합하지 않는다. C의105는 미확인1건이지 0원 확정이 아니다. D의 기본 LEFT JOIN은6행이며40은 NULL 확장1행이지만 실제 접수는0건이다. 정원은 여러 접수행의 합에 대한 규칙이라 개별 seats CHECK만으로 보장되지 않는다. 동시 입력에는 같은 거래의 조건 검사·잠금·격리 정책이 필요하다. 오답노트에는 문제번호, 내 SQL, 예측행, 실제행, 첫 차이가 발생한 절, 원인(키·NULL·필터·행수·함수·집계), 수정 SQL, 새 입력 재시험을 쓴다. 맞힌 경우에는 “오류 없음”과 검증 근거를 기록한다. example과 tests는 먼저 숨기고 푼다. 자체 창작 훈련이며 기출 복제가 아니다. 각 과제의 정확값·중간행·원인 설명·변형 재시험을 구분해 평가한다. 전부 충족한 과제만 통과한다. 이 네 과제는 공식 형식의 50문항·90분 모의시험이 아니며 50문항으로 계산하지 않는다. 별도 모의시험과 연결해 취약 유형을 재학습하되 객관식 점수와 SQL 독립 작성은 따로 기록한다. 이 단원에는 A~D 실기와 구별되는 자체 모의 50문항도 내장되어 있다. exam-paper.txt를 먼저 열어90분 동안 풀이한다. 1~10은 모델링,11~50은SQL이며 문항당2점이다. 총60점 이상·모델링8/20점 이상·SQL32/80점 이상은 자체 진단기준일 뿐 공식 합격이나 숙련 인증이 아니다. 두 회차는 계산문항의 입력을 바꾸고 개념/관리 문항을 달리한 병렬형으로, 동일 원리를 새 입력에 적용하는지 점검한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. 시험은 exam-paper.txt와 각 문제의 독립 입력만 보고 먼저 풀이한다. 학습자가 타이머로90분을 잰다. 답안을 exam-response.json의0 대신 선택지1~4로 입력한 뒤 python3 exam-grade.py로 자체 채점한다. 미응답은0 그대로 둔다.
  6. 제출 뒤 exam-answers.txt로 문항별 이유를 확인하고 python3 exam-verify.py로30개 계산문항을 실제 SQLite 검산한다. exam-cases.json은 공개 해설용 정답이므로 시험 중에는 가린다. python3 exam-grade.py --self-test는 채점 경계를 점검한다.
  7. 틀린 문항마다 원인과 고친 SQL/모델을 error-notes.md에 적는다. 입력 한 값을 직접 바꾸어 예측→실행→중간값 대조를 다시 한다. 자동 객관식 점수와 A~D 실기 rubric은 합치지 않는다.

입력 예제

INSERT INTO class VALUES(50,2,'가죽 장식',2); INSERT INTO booking VALUES(106,50,'바람',2,'완료',9000),(107,50,'새롬',1,'취소',9000); SELECT b.id,CASE WHEN b.status='완료' THEN b.seats ELSE 0 END FROM booking b WHERE class_id=50 ORDER BY b.id; SELECT c.id,SUM(CASE WHEN b.status='완료' THEN b.seats ELSE 0 END) used,c.capacity FROM class c LEFT JOIN booking b ON b.class_id=c.id WHERE c.id=50 GROUP BY c.id,c.capacity;

예상 결과

[[[106, 2], [107, 0]], [[50, 2, 2]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. A~D 각각 정확값·중간행·원인 설명·변형 검증 네 칸을 모두 충족해야 통과한다. 하나라도 빠지면 해당 유형만 복습하고 새 입력으로 재시험한다. 공식시험 합격이나 전과목 숙련으로 판정하지 않는다. A의 PK/FK와 강좌명 갱신 이상 설명도 필수다.

흔한 오해와 실패 복구

  1. 자체 네 과제를 50문항·90분 모의시험 완료로 표시하지 않는다.
  2. class20의 정원4와 다른 미니 예제의 정원을 혼합하지 않는다.

확인 문제

신규 class50에서 취소 좌석까지 합하면 어떤 오답이 생기는가?

자기평가 · 자동채점 아님

평가 기준: 106 완료2석,107 취소1석, 올바른 합2와 잘못된 합3, 정원2를 함께 제시하면 통과.

해설 보기

사용3석으로 계산하여 정원2를 초과했다고 잘못 판단한다. 완료106의2석만 합해야 한다.

대여(id,공구id,공구명)에서 공구id→공구명이고 id가 주키다. 이름 갱신 이상을 줄이는 설계는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

id→공구id→공구명의 이행종속을 분리하고 FK로 연결하면 공구 이름은 한 곳에서 고친다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 공구(공구id,공구명)와 대여(id,공구id)로 분리

주문상품의 후보키가(주문id,상품id)이고 상품id→상품명이다. 가장 직접적인 위반은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

상품명은 복합후보키 전체가 아니라 상품id에만 의존한다. 상품표로 분리한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 2NF의 부분 함수 종속

공구는 한 번도 대여되지 않을 수 있고, 대여는 공구 하나를 반드시 가리킨다. 대응수와 선택사양은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

공구의 참여는 선택, 대여의 부모 참조는 필수다. 부모가 자식0개인 것과 FK NULL을 혼동하지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 공구1 : 대여0..N, 대여의 공구 FK는 필수

고객번호를 인조키로 추가했다. 중복 이메일을 금지하려면?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

인조키의 유일성과 업무 후보키의 유일성은 별개다. 이메일 유일성이 실제 업무규칙이라는 전제를 먼저 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 업무상 유일한 이메일에 별도 UNIQUE도 선언

하나의 품목 칸에 '우산,장갑' 목록을 넣었다. 원자적 품목 조회를 위해 먼저 할 일은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

정한 도메인에서 여러 품목을 한 칸에 넣지 않고 거래id·품목id로 분해한다. 단순 구분자 변경은 구조를 고치지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 거래품목 연결표에 품목 하나당 한 행 저장

조회용 뷰는 그대로 두고 내부 인덱스를 추가해도 앱 조회계약이 유지됐다. 어떤 예인가?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

저장·접근 구조 변경을 외부 조회 계약과 분리한 예다. 실제 성능 개선은 측정해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 물리적 데이터 독립성

(학생S,과목C,강사I)에 SC→I, I→C만 있고 후보키 SC/SI다. 3NF일 수 있어도 BCNF가 아닌 이유는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

C는 후보키에 속한 주요 속성이라3NF 조건은 가능하지만, BCNF는 모든 비자명 종속의 결정자가 슈퍼키여야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: I→C에서 I가 슈퍼키가 아니다

같은 키에 대해 딱 한 행을 식별해야 한다. 후보키의 최소성은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

슈퍼키 중 불필요한 속성을 뺄 수 없는 것이 후보키다. 표시 길이나 타입과 다르다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 어떤 속성도 빼면 유일성이 보장되지 않는다

booking.class_id에 NOT NULL FK가 있다. 직접 보장하는 것은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

부모 참조와 필수값만 보장한다. 여러 행의 좌석합은 별도 업무규칙이다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 각 접수가 존재하는 강좌를 참조함

반정규화로 완료좌석 합을 저장한 뒤 접수를 바꾸었더니 합이 오래됐다. 해결 방향은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

중복 저장은 갱신 책임을 만든다. 원본 재계산과 대조하고 성능 이득·쓰기 비용을 측정해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 갱신 일관성 정책과 측정 근거를 먼저 정의

자체 모의 1회 11번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT COUNT(*),COUNT(v) FROM t;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

COUNT(*)는 NULL행도 포함하고 COUNT(v)는 v가 있는 네 행만 센다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[5,4]]

자체 모의 1회 12번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT SUM(v),AVG(v) FROM t;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

비NULL 네 값의 합을4로 나눈다. NULL을0으로 채운 평균과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[55,13.75]]

자체 모의 1회 13번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE v=NULL ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

NULL과 =로 비교한 결과는 UNKNOWN이다. WHERE는 TRUE만 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: []

자체 모의 1회 14번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE v IS NULL ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

IS NULL은 결측을 직접 검사한다. id4만 결측이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[4]]

자체 모의 1회 15번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE v BETWEEN 10 AND 20 ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

BETWEEN은 양끝을 포함한다. 범위 밖과 NULL은 남지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1],[2],[3]]

자체 모의 1회 16번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE grp='A' OR grp='B' AND flag='ok' ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

AND가 OR보다 먼저 묶인다. A는 모두, B는 ok만 통과하지만 B에는 ok가 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1],[2]]

자체 모의 1회 17번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE id NOT IN (1,NULL) ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

1 이외의 값도 NULL과 다르다는 비교가 UNKNOWN이다. NULL을 제외하거나 NOT EXISTS로 고친다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: []

자체 모의 1회 18번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,CASE WHEN v IS NULL THEN 'missing' WHEN v>=20 THEN 'high' ELSE 'low' END FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

CASE는 처음 TRUE가 되는 분기를 택한다. NULL을 먼저 별도로 표시한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,"low"],[2,"high"],[3,"high"],[4,"missing"],[5,"low"]]

자체 모의 1회 19번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,COALESCE(v,0),NULLIF(v,20) FROM t WHERE id IN (3,4) ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

NULLIF는20과 같을 때만 NULL로 바꾼다. 원본 NULL은 유지되고 COALESCE만 표시0을 만든다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[3,20,null],[4,0,null]]

자체 모의 1회 20번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT grp,COUNT(*),SUM(v) FROM t GROUP BY grp ORDER BY grp;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

A 두 행, B 두 행, C 한 행이다. B의 합은 결측을 빼고 계산하지만 COUNT(*)는2다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["A",2,30],["B",2,20],["C",1,5]]

자체 모의 1회 21번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT grp,SUM(v) FROM t GROUP BY grp HAVING SUM(v)>=20 ORDER BY grp;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

HAVING은 그룹합이 만들어진 뒤 필터한다. WHERE로 SUM 조건을 옮길 수 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["A",30],["B",20]]

자체 모의 1회 22번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT c.id,p.id FROM t c JOIN t p ON c.parent=p.id ORDER BY c.id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

부모가 있는2·3·4만 연결된다. 자기조인은 같은 표를 역할별 별칭으로 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[2,1],[3,1],[4,2]]

자체 모의 1회 23번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT c.id,p.id FROM t c LEFT JOIN t p ON c.parent=p.id WHERE c.id IN (1,4,5) ORDER BY c.id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

부모 없는1·5도 LEFT라 남는다. 부모열만 NULL로 확장된다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,null],[4,2],[5,null]]

자체 모의 1회 24번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT COUNT(*) FROM t a CROSS JOIN t b WHERE a.id<=2 AND b.id<=3;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

왼쪽2행과 오른쪽3행의 모든 짝은2×3=6개다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[6]]

자체 모의 1회 25번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT p.id FROM t p WHERE EXISTS (SELECT 1 FROM t c WHERE c.parent=p.id) ORDER BY p.id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

자식이 둘인1도 한 행이다. EXISTS는 바깥 행마다 존재 여부만 판정한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1],[2]]

자체 모의 1회 26번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE (grp,v) IN (SELECT grp,MAX(v) FROM t GROUP BY grp) ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

그룹별 최대와 (그룹,값) 쌍을 비교한다. NULL행은 최대값의 대표가 아니다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[2],[3],[5]]

자체 모의 1회 27번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT v FROM t WHERE id<=3 UNION SELECT v FROM t WHERE id=5 ORDER BY v;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

v만 선택했으므로20 또는15의 중복이 제거된다. id까지 선택하면 다른 결과다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[5],[10],[20]]

자체 모의 1회 28번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT COUNT(*) FROM (SELECT v FROM t WHERE id<=3 UNION ALL SELECT v FROM t WHERE id=5);

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

UNION ALL은 중복까지 보존해3+1=4행이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[4]]

자체 모의 1회 29번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT v FROM t WHERE grp='A' INTERSECT SELECT v FROM t WHERE grp='B' ORDER BY v;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

A와 B 모두에서 나타난 값만 하나씩 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[20]]

자체 모의 1회 30번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT v FROM t WHERE grp='A' EXCEPT SELECT v FROM t WHERE grp='B' ORDER BY v;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

A에만 있는 값이다. Oracle19c의 같은 목적 구문은 MINUS다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[10]]

자체 모의 1회 31번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,RANK() OVER(ORDER BY v DESC),DENSE_RANK() OVER(ORDER BY v DESC) FROM t WHERE v IS NOT NULL ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

동점 다음 RANK는 건너뛰고 DENSE_RANK는 서로 다른 값의 순서다. NULL은 계산 전에 제외했다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,3,2],[2,1,1],[3,1,1],[5,4,3]]

자체 모의 1회 32번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,SUM(COALESCE(v,0)) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

id 순서로 누적하며4의 NULL을0으로 표시했다. 이 표시정책이 실제 값0을 증명하지는 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,10],[2,30],[3,50],[4,50],[5,55]]

자체 모의 1회 33번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,LAG(v) OVER(ORDER BY id),LEAD(v) OVER(ORDER BY id) FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

이전/다음 행으로 이동한다. 경계와 원자료 NULL 모두 NULL 결과를 만들므로 행 위치와 같이 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,null,20],[2,10,20],[3,20,null],[4,20,5],[5,null,null]]

자체 모의 1회 34번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,LAST_VALUE(v) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

프레임을 끝까지 펼쳤으므로 모든 행의 LAST_VALUE가 id5의 값이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,5],[2,5],[3,5],[4,5],[5,5]]

자체 모의 1회 35번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,v FROM t WHERE v IS NOT NULL ORDER BY v DESC,id LIMIT 2;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

정확히2행이며 동점은id 오름차순으로 푼다. 동점 모두 포함하는 등수 조건과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[2,20],[3,20]]

자체 모의 1회 36번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. WITH RECURSIVE r(id,depth) AS (SELECT id,1 FROM t WHERE id=1 UNION ALL SELECT t.id,r.depth+1 FROM t JOIN r ON t.parent=r.id WHERE r.depth<5) SELECT * FROM r ORDER BY depth,id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

뿌리1→자식2·3→손자4 순서다. 이 입력은 순환이 없으며 깊이제한만으로 일반 트리 정제를 보장하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,1],[2,2],[3,2],[4,3]]

자체 모의 1회 37번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT SUM(CASE WHEN grp='A' THEN v END),SUM(CASE WHEN grp='B' THEN v END),SUM(CASE WHEN grp='C' THEN v END) FROM t;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

조건별 합계를 가로로 펼친다. 중복 원행과 순서는 집계 과정에서 사라진다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[30,20,5]]

자체 모의 1회 38번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT UPPER('ab'),SUBSTR('A-12',3,2),REPLACE('A-12','-',''),ROUND(2.346,2);

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

대문자화·1기반 세번째부터2글자·하이픈 제거·소수둘째자리 반올림을 각각 계산한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["AB","12","A12",2.35]]

자체 모의 1회 39번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT date('2024-02-28','+1 day'),date('2025-02-28','+1 day'),CAST('12' AS INTEGER)+1;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

2024는 윤년,2025는 평년이다. 숫자문자는 명시적으로 변환한다. SQLite 날짜함수를 Oracle에 그대로 복사하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["2024-02-29","2025-03-01",13]]

자체 모의 1회 40번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,ROUND(1.0*v/SUM(v) OVER(PARTITION BY grp),3) FROM t WHERE grp='A' ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

같은 그룹 합을 분모로 쓴다. 정수 나눗셈을 피하려고1.0을 곱한다. 0합이면 NULLIF 등의 정책을 추가한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,0.333],[2,0.667]]

Oracle NVL2(x,'있음','없음')의 x가NULL이면?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

NVL2는 x의 NULL 여부에 따라 두 번째/세 번째 인수를 고른다. SQLite는 CASE WHEN x IS NOT NULL로 목적을 재현한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 없음

Oracle ROWNUM<=2와 같은 SELECT의 ORDER BY 금액 DESC를 함께 썼다. Top2를 보장하나?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

ROWNUM으로 먼저 고른 행만 정렬할 수 있다. 정확한 순서·동점정책을 선언해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 아니다. 정렬된 인라인 뷰 바깥 ROWNUM 또는 FETCH 사용

Oracle ROLLUP(지역,채널)이 만드는 집계수준은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

왼쪽 접두 열을 차례로 유지하고 마지막은 총계다. 채널만의 소계는 없다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: (지역,채널), (지역), ()

실제 지역NULL 그룹과 총계를 구별하는 Oracle GROUPING(지역) 값은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

값이 비었는지가 아니라 그 열이 해당 집계수준에서 제외되었는지를 표시한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 실제NULL 0, 집계로 생긴NULL 1

Oracle START WITH id=1 CONNECT BY PRIOR id=parent_id에서 PRIOR id는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

부모 id와 자식 parent_id를 연결한다. LEVEL은 시작행1부터 세는 깊이다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 부모 행의 id

A-123에서 숫자123을 REGEXP_SUBSTR로 뽑았다. A-두자리 형식 검증을 통과했나?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

REGEXP_LIKE의 경계·자리수·대소문자 정책을 따로 검사해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 아니다. 추출 성공은 전체 형식 검증이 아니다

BEGIN 뒤 SAVEPOINT s, UPDATE, ROLLBACK TO s를 SQLite에서 실행했다. 거래는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

저장점 뒤만 취소한다. 전체 ROLLBACK과 다르며 필요하면 RELEASE 후 COMMIT/ROLLBACK을 명시한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: UPDATE는 취소되지만 바깥 거래는 계속된다

Oracle 일반표 TRUNCATE를 실행한 뒤 ROLLBACK으로 행을 되살릴 수 있나?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

Oracle 일반표 TRUNCATE는 전행 절단 DDL이며 되돌리기용 거래에 넣으면 안 된다. SQLite는 같은 구문이 없다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 아니다. 일반 DDL의 커밋 규칙을 따른다

SQLite UPSERT와 Oracle MERGE가 같은 범위의 기능인가?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

단순 키기반 예제 결과를 비교할 수 있지만 임의 ON조건과 관리 동작까지 전부 등가라고 할 수 없다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 아니다. 전자는 UNIQUE/PK 충돌, 후자는 USING/ON 매칭을 사용

역할의 SELECT를 REVOKE했는데 사용자에게 직접SELECT가 남아 있다. 접근은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

회수는 해당 부여 경로를 끊는다. 역할·직접권한·다른 경로를 검증해야 하며 SQLite 정책표는 실제권한 집행이 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 다른 허용 경로를 확인해야 한다

독립 실습

A~D를 해설 없이 작성하고 과제마다 오답노트 한 행을 남겨라. 맞혔다면 검증 근거를 기록한다. B의 신규 좌석을4에서3으로 바꾸어 재시험하고, 실제 INSERT를 하지 않았음을 행수로 증명하라. A에서는 class와 booking의 PK/FK를 표시하고 booking에 강좌명을 복사하지 않는 이유도 적어라. 별도의 exam-paper.txt 50문항도 풀고 자체 점수와 A~D 독립 수행 결과를 구분하여 제출하라.

남길 증거: 독립 SQL, 중간행, 입력변형 결과, 오답노트와 근거. A~D 각각 정확값·중간행·원인 설명·변형 검증 네 칸을 모두 충족해야 통과한다. 하나라도 빠지면 해당 유형만 복습하고 새 입력으로 재시험한다. 공식시험 합격이나 전과목 숙련으로 판정하지 않는다. A의 PK/FK와 강좌명 갱신 이상 설명도 필수다. 50문항 답안·90분 사용기록·과목별 자체 점수·오답별 수정근거를 별도로 제출한다.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: diagnostic-a
INSERT INTO class VALUES(50,2,'가죽 장식',2); INSERT INTO booking VALUES(106,50,'바람',2,'완료',9000),(107,50,'새롬',1,'취소',9000); SELECT b.id,CASE WHEN b.status='완료' THEN b.seats ELSE 0 END FROM booking b WHERE class_id=50 ORDER BY b.id; SELECT c.id,SUM(CASE WHEN b.status='완료' THEN b.seats ELSE 0 END) used,c.capacity FROM class c LEFT JOIN booking b ON b.class_id=c.id WHERE c.id=50 GROUP BY c.id,c.capacity;
-- case: diagnostic-b
SELECT c.id,COALESCE(SUM(b.seats),0) existing,4 requested,c.capacity,CASE WHEN COALESCE(SUM(b.seats),0)+4>c.capacity THEN '거절' ELSE '허용' END FROM class c LEFT JOIN booking b ON b.class_id=c.id AND b.status='완료' WHERE c.id=20 GROUP BY c.id,c.capacity; SELECT COUNT(*) FROM booking;
-- case: diagnostic-c-d
SELECT COUNT(*),COUNT(paid),SUM(CASE WHEN paid IS NULL THEN 1 ELSE 0 END),COALESCE(MAX(CASE WHEN id=105 THEN paid END),0) FROM booking; SELECT COUNT(*) FROM class c LEFT JOIN booking b ON b.class_id=c.id; SELECT c.id,COUNT(*),COUNT(b.id) FROM class c LEFT JOIN booking b ON b.class_id=c.id WHERE c.id=40 GROUP BY c.id;
-- case: variant-request-three
SELECT c.id,SUM(b.seats)+3,c.capacity,CASE WHEN SUM(b.seats)+3>c.capacity THEN '거절' ELSE '허용' END FROM class c JOIN booking b ON b.class_id=c.id AND b.status='완료' WHERE c.id=20 GROUP BY c.id,c.capacity;

SELECT별 고정 정답과 오류 유형

{
  "diagnostic-a": [
    [
      [
        106,
        2
      ],
      [
        107,
        0
      ]
    ],
    [
      [
        50,
        2,
        2
      ]
    ]
  ],
  "diagnostic-b": [
    [
      [
        20,
        1,
        4,
        4,
        "거절"
      ]
    ],
    [
      [
        5
      ]
    ]
  ],
  "diagnostic-c-d": [
    [
      [
        5,
        4,
        1,
        0
      ]
    ],
    [
      [
        6
      ]
    ],
    [
      [
        40,
        1,
        0
      ]
    ]
  ],
  "variant-request-three": [
    [
      [
        20,
        4,
        4,
        "허용"
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

독립 진단 오답노트 양식

# 자체 독립 진단 오답노트
공식 기출·공식 시험 성적 아님. 각 과제는 해설을 가리고 풀이한다.

|과제|내 SQL|예측 행|실제 행|첫 오류 절|원인|수정 SQL|새 입력|재시험 결과|
|---|---|---|---|---|---|---|---|---|
|A|||||||||
|B|||||||||
|C|||||||||
|D|||||||||

맞혔으면 원인 칸에 오류 없음과 검증 근거를 적는다.
객관식 자체 50문항 시험은 별도 연계하며 이 네 과제를 50문항으로 세지 않는다.

실제 검산한 Python stdout (SQLite; Oracle 제외)

diagnostic-a|[[[106,2],[107,0]],[[50,2,2]]]
diagnostic-b|[[[20,1,4,4,"거절"]],[[5]]]
diagnostic-c-d|[[[5,4,1,0]],[[6]],[[40,1,0]]]
variant-request-three|[[[20,4,4,"허용"]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')

자체 50문항 문제지·90분 자가진단

AX4U 독자 모의 1회 — 원기출 복원 아님 / 공식 합격 보장 없음
가상 데이터만 사용. 시험시간 90분은 학습자가 타이머로 잰다. 웹 자동 시간제한 기능을 뜻하지 않는다.
1~10: 모델링20점, 11~50: SQL80점, 문항당2점. 두 회차는 병렬형: 계산30문항의 수요값을 바꾸고 모델링·방언/관리20문항은 다른 질문을 사용한다.
총60점 이상이면서 모델링8점 이상, SQL32점 이상을 자체 진단기준으로 사용. 독립 SQL 작성과 오류복구는 기존 A~D 실기과제로 별도평가.
문제지와 seed만 먼저 열고 해설·exam-cases.json·코드는 제출 뒤 확인한다.


1. 대여(id,공구id,공구명)에서 공구id→공구명이고 id가 주키다. 이름 갱신 이상을 줄이는 설계는?
1) 대여id만 매번 새로 발급
2) 공구명을 매 행 자유입력
3) 공구id를 삭제
4) 공구(공구id,공구명)와 대여(id,공구id)로 분리

2. 주문상품의 후보키가(주문id,상품id)이고 상품id→상품명이다. 가장 직접적인 위반은?
1) 무조건 BCNF만의 문제
2) 외래키가 많아서 생기는 문제
3) 2NF의 부분 함수 종속
4) 모든 열이 NULL이라는 문제

3. 공구는 한 번도 대여되지 않을 수 있고, 대여는 공구 하나를 반드시 가리킨다. 대응수와 선택사양은?
1) 공구와 대여는 항상1:1
2) 공구1 : 대여0..N, 대여의 공구 FK는 필수
3) 공구1 : 대여1..1
4) 대여는 공구 없이 허용

4. 고객번호를 인조키로 추가했다. 중복 이메일을 금지하려면?
1) 업무상 유일한 이메일에 별도 UNIQUE도 선언
2) 인조키가 이메일 중복도 자동 금지
3) 모든 이메일을 NULL로 바꿈
4) 이메일마다 같은 고객번호 사용

5. 하나의 품목 칸에 '우산,장갑' 목록을 넣었다. 원자적 품목 조회를 위해 먼저 할 일은?
1) 문자열을 더 길게 허용
2) 쉼표를 공백으로만 변경
3) 목록 전체에 랜덤 번호 부여
4) 거래품목 연결표에 품목 하나당 한 행 저장

6. 조회용 뷰는 그대로 두고 내부 인덱스를 추가해도 앱 조회계약이 유지됐다. 어떤 예인가?
1) 사용자 인증
2) 원자성 위반
3) 물리적 데이터 독립성
4) 무조건 논리 모델 폐기

7. (학생S,과목C,강사I)에 SC→I, I→C만 있고 후보키 SC/SI다. 3NF일 수 있어도 BCNF가 아닌 이유는?
1) 복합키는 무조건 BCNF 위반
2) I→C에서 I가 슈퍼키가 아니다
3) 모든 결정자는 항상 슈퍼키
4) C가 NULL이라서

8. 같은 키에 대해 딱 한 행을 식별해야 한다. 후보키의 최소성은?
1) 어떤 속성도 빼면 유일성이 보장되지 않는다
2) 글자 수가 가장 짧다
3) 숫자형만 허용한다
4) 모든 열을 포함한다

9. booking.class_id에 NOT NULL FK가 있다. 직접 보장하는 것은?
1) 강좌별 합계가 정원을 넘지 않음
2) 모든 강좌에 접수가 반드시 있음
3) 각 접수의 신청자가 유일함
4) 각 접수가 존재하는 강좌를 참조함

10. 반정규화로 완료좌석 합을 저장한 뒤 접수를 바꾸었더니 합이 오래됐다. 해결 방향은?
1) 원본을 무조건 삭제
2) 조회마다 개인정보를 더 수집
3) 갱신 일관성 정책과 측정 근거를 먼저 정의
4) 캐시는 언제나 원본보다 정확

11. 자체 모의 1회 11번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT COUNT(*),COUNT(v) FROM t;
1) [[null,null]]
2) [[5,4]]
3) [[5,4],[5,4]]
4) [[6,5]]

12. 자체 모의 1회 12번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT SUM(v),AVG(v) FROM t;
1) [[55,13.75]]
2) [[55,13.75],[55,13.75]]
3) [[56,14.75]]
4) [[null,null]]

13. 자체 모의 1회 13번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE v=NULL ORDER BY id;
1) [[null]]
2) [[0]]
3) [[1]]
4) []

14. 자체 모의 1회 14번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE v IS NULL ORDER BY id;
1) [[5]]
2) [[null]]
3) [[4]]
4) [[4],[4]]

15. 자체 모의 1회 15번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE v BETWEEN 10 AND 20 ORDER BY id;
1) [[null],[null],[null]]
2) [[1],[2],[3]]
3) [[1],[2]]
4) [[2],[3],[4]]

16. 자체 모의 1회 16번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE grp='A' OR grp='B' AND flag='ok' ORDER BY id;
1) [[1],[2]]
2) [[1]]
3) [[2],[3]]
4) [[null],[null]]

17. 자체 모의 1회 17번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE id NOT IN (1,NULL) ORDER BY id;
1) [[null]]
2) [[0]]
3) [[1]]
4) []

18. 자체 모의 1회 18번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,CASE WHEN v IS NULL THEN 'missing' WHEN v>=20 THEN 'high' ELSE 'low' END FROM t ORDER BY id;
1) [[2,"LOW"],[3,"HIGH"],[4,"HIGH"],[5,"MISSING"],[6,"LOW"]]
2) [[null,null],[null,null],[null,null],[null,null],[null,null]]
3) [[1,"low"],[2,"high"],[3,"high"],[4,"missing"],[5,"low"]]
4) [[1,"low"],[2,"high"],[3,"high"],[4,"missing"]]

19. 자체 모의 1회 19번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,COALESCE(v,0),NULLIF(v,20) FROM t WHERE id IN (3,4) ORDER BY id;
1) [[null,null,0],[null,null,0]]
2) [[3,20,null],[4,0,null]]
3) [[3,20,null]]
4) [[4,21,0],[5,1,0]]

20. 자체 모의 1회 20번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT grp,COUNT(*),SUM(v) FROM t GROUP BY grp ORDER BY grp;
1) [["A",2,30],["B",2,20],["C",1,5]]
2) [["A",2,30],["B",2,20]]
3) [["a",3,31],["b",3,21],["c",2,6]]
4) [[null,null,null],[null,null,null],[null,null,null]]

21. 자체 모의 1회 21번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT grp,SUM(v) FROM t GROUP BY grp HAVING SUM(v)>=20 ORDER BY grp;
1) [["A",30]]
2) [["a",31],["b",21]]
3) [[null,null],[null,null]]
4) [["A",30],["B",20]]

22. 자체 모의 1회 22번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT c.id,p.id FROM t c JOIN t p ON c.parent=p.id ORDER BY c.id;
1) [[3,2],[4,2],[5,3]]
2) [[null,null],[null,null],[null,null]]
3) [[2,1],[3,1],[4,2]]
4) [[2,1],[3,1]]

23. 자체 모의 1회 23번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT c.id,p.id FROM t c LEFT JOIN t p ON c.parent=p.id WHERE c.id IN (1,4,5) ORDER BY c.id;
1) [[null,0],[null,null],[null,0]]
2) [[1,null],[4,2],[5,null]]
3) [[1,null],[4,2]]
4) [[2,0],[5,3],[6,0]]

24. 자체 모의 1회 24번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT COUNT(*) FROM t a CROSS JOIN t b WHERE a.id<=2 AND b.id<=3;
1) [[6]]
2) [[6],[6]]
3) [[7]]
4) [[null]]

25. 자체 모의 1회 25번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT p.id FROM t p WHERE EXISTS (SELECT 1 FROM t c WHERE c.parent=p.id) ORDER BY p.id;
1) [[1]]
2) [[2],[3]]
3) [[null],[null]]
4) [[1],[2]]

26. 자체 모의 1회 26번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE (grp,v) IN (SELECT grp,MAX(v) FROM t GROUP BY grp) ORDER BY id;
1) [[3],[4],[6]]
2) [[null],[null],[null]]
3) [[2],[3],[5]]
4) [[2],[3]]

27. 자체 모의 1회 27번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT v FROM t WHERE id<=3 UNION SELECT v FROM t WHERE id=5 ORDER BY v;
1) [[null],[null],[null]]
2) [[5],[10],[20]]
3) [[5],[10]]
4) [[6],[11],[21]]

28. 자체 모의 1회 28번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT COUNT(*) FROM (SELECT v FROM t WHERE id<=3 UNION ALL SELECT v FROM t WHERE id=5);
1) [[4]]
2) [[4],[4]]
3) [[5]]
4) [[null]]

29. 자체 모의 1회 29번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT v FROM t WHERE grp='A' INTERSECT SELECT v FROM t WHERE grp='B' ORDER BY v;
1) [[20],[20]]
2) [[21]]
3) [[null]]
4) [[20]]

30. 자체 모의 1회 30번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT v FROM t WHERE grp='A' EXCEPT SELECT v FROM t WHERE grp='B' ORDER BY v;
1) [[11]]
2) [[null]]
3) [[10]]
4) [[10],[10]]

31. 자체 모의 1회 31번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,RANK() OVER(ORDER BY v DESC),DENSE_RANK() OVER(ORDER BY v DESC) FROM t WHERE v IS NOT NULL ORDER BY id;
1) [[null,null,null],[null,null,null],[null,null,null],[null,null,null]]
2) [[1,3,2],[2,1,1],[3,1,1],[5,4,3]]
3) [[1,3,2],[2,1,1],[3,1,1]]
4) [[2,4,3],[3,2,2],[4,2,2],[6,5,4]]

32. 자체 모의 1회 32번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,SUM(COALESCE(v,0)) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t ORDER BY id;
1) [[1,10],[2,30],[3,50],[4,50],[5,55]]
2) [[1,10],[2,30],[3,50],[4,50]]
3) [[2,11],[3,31],[4,51],[5,51],[6,56]]
4) [[null,null],[null,null],[null,null],[null,null],[null,null]]

33. 자체 모의 1회 33번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,LAG(v) OVER(ORDER BY id),LEAD(v) OVER(ORDER BY id) FROM t ORDER BY id;
1) [[1,null,20],[2,10,20],[3,20,null],[4,20,5]]
2) [[2,0,21],[3,11,21],[4,21,0],[5,21,6],[6,0,0]]
3) [[null,0,null],[null,null,null],[null,null,0],[null,null,null],[null,0,0]]
4) [[1,null,20],[2,10,20],[3,20,null],[4,20,5],[5,null,null]]

34. 자체 모의 1회 34번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,LAST_VALUE(v) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t ORDER BY id;
1) [[2,6],[3,6],[4,6],[5,6],[6,6]]
2) [[null,null],[null,null],[null,null],[null,null],[null,null]]
3) [[1,5],[2,5],[3,5],[4,5],[5,5]]
4) [[1,5],[2,5],[3,5],[4,5]]

35. 자체 모의 1회 35번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,v FROM t WHERE v IS NOT NULL ORDER BY v DESC,id LIMIT 2;
1) [[null,null],[null,null]]
2) [[2,20],[3,20]]
3) [[2,20]]
4) [[3,21],[4,21]]

36. 자체 모의 1회 36번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
WITH RECURSIVE r(id,depth) AS (SELECT id,1 FROM t WHERE id=1 UNION ALL SELECT t.id,r.depth+1 FROM t JOIN r ON t.parent=r.id WHERE r.depth<5) SELECT * FROM r ORDER BY depth,id;
1) [[1,1],[2,2],[3,2],[4,3]]
2) [[1,1],[2,2],[3,2]]
3) [[2,2],[3,3],[4,3],[5,4]]
4) [[null,null],[null,null],[null,null],[null,null]]

37. 자체 모의 1회 37번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT SUM(CASE WHEN grp='A' THEN v END),SUM(CASE WHEN grp='B' THEN v END),SUM(CASE WHEN grp='C' THEN v END) FROM t;
1) [[30,20,5],[30,20,5]]
2) [[31,21,6]]
3) [[null,null,null]]
4) [[30,20,5]]

38. 자체 모의 1회 38번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT UPPER('ab'),SUBSTR('A-12',3,2),REPLACE('A-12','-',''),ROUND(2.346,2);
1) [["ab","","a12",3.35]]
2) [[null,null,null,null]]
3) [["AB","12","A12",2.35]]
4) [["AB","12","A12",2.35],["AB","12","A12",2.35]]

39. 자체 모의 1회 39번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT date('2024-02-28','+1 day'),date('2025-02-28','+1 day'),CAST('12' AS INTEGER)+1;
1) [[null,null,null]]
2) [["2024-02-29","2025-03-01",13]]
3) [["2024-02-29","2025-03-01",13],["2024-02-29","2025-03-01",13]]
4) [["","",14]]

40. 자체 모의 1회 40번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",10,null,"ok"],[2,"A",20,1,"ok"],[3,"B",20,1,"no"],[4,"B",null,2,null],[5,"C",5,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,ROUND(1.0*v/SUM(v) OVER(PARTITION BY grp),3) FROM t WHERE grp='A' ORDER BY id;
1) [[1,0.333],[2,0.667]]
2) [[1,0.333]]
3) [[2,1.333],[3,1.667]]
4) [[null,null],[null,null]]

41. Oracle NVL2(x,'있음','없음')의 x가NULL이면?
1) 있음
2) 0
3) 무조건 오류
4) 없음

42. Oracle ROWNUM<=2와 같은 SELECT의 ORDER BY 금액 DESC를 함께 썼다. Top2를 보장하나?
1) ROWNUM은 자동 내림차순
2) ORDER BY를 없애면 해결
3) 아니다. 정렬된 인라인 뷰 바깥 ROWNUM 또는 FETCH 사용
4) 항상 상위2개

43. Oracle ROLLUP(지역,채널)이 만드는 집계수준은?
1) CUBE와 항상 같은 수준
2) (지역,채널), (지역), ()
3) (채널)만
4) (지역,채널)만

44. 실제 지역NULL 그룹과 총계를 구별하는 Oracle GROUPING(지역) 값은?
1) 실제NULL 0, 집계로 생긴NULL 1
2) 둘 다1
3) 둘 다0
4) 실제NULL 1, 총계0

45. Oracle START WITH id=1 CONNECT BY PRIOR id=parent_id에서 PRIOR id는?
1) 항상 현재 자식의 id
2) 최대 id
3) 정렬 순위
4) 부모 행의 id

46. A-123에서 숫자123을 REGEXP_SUBSTR로 뽑았다. A-두자리 형식 검증을 통과했나?
1) 세자리도 두자리와 동일
2) NULL만 아니면 통과
3) 아니다. 추출 성공은 전체 형식 검증이 아니다
4) 숫자를 뽑으면 통과

47. BEGIN 뒤 SAVEPOINT s, UPDATE, ROLLBACK TO s를 SQLite에서 실행했다. 거래는?
1) 표가 삭제됨
2) UPDATE는 취소되지만 바깥 거래는 계속된다
3) 자동으로 COMMIT됨
4) 모든 이전 거래도 취소

48. Oracle 일반표 TRUNCATE를 실행한 뒤 ROLLBACK으로 행을 되살릴 수 있나?
1) 아니다. 일반 DDL의 커밋 규칙을 따른다
2) 항상 복구 가능
3) WHERE로 한 행만 복구
4) SQLite와 구문·동작 모두 동일

49. SQLite UPSERT와 Oracle MERGE가 같은 범위의 기능인가?
1) 완전히 동일
2) MERGE는 SELECT만 수행
3) UPSERT는 모든 권한을 부여
4) 아니다. 전자는 UNIQUE/PK 충돌, 후자는 USING/ON 매칭을 사용

50. 역할의 SELECT를 REVOKE했는데 사용자에게 직접SELECT가 남아 있다. 접근은?
1) 모든 DB가 삭제됨
2) 역할을 지우면 모든 직접권한도 제거
3) 다른 허용 경로를 확인해야 한다
4) 무조건 차단

문항별 정답·구체 해설

AX4U 독자 모의 1회 — 원기출 복원 아님 / 공식 합격 보장 없음
가상 데이터만 사용. 시험시간 90분은 학습자가 타이머로 잰다. 웹 자동 시간제한 기능을 뜻하지 않는다.
1~10: 모델링20점, 11~50: SQL80점, 문항당2점. 두 회차는 병렬형: 계산30문항의 수요값을 바꾸고 모델링·방언/관리20문항은 다른 질문을 사용한다.
총60점 이상이면서 모델링8점 이상, SQL32점 이상을 자체 진단기준으로 사용. 독립 SQL 작성과 오류복구는 기존 A~D 실기과제로 별도평가.
문제지와 seed만 먼저 열고 해설·exam-cases.json·코드는 제출 뒤 확인한다.

1. 정답 4
id→공구id→공구명의 이행종속을 분리하고 FK로 연결하면 공구 이름은 한 곳에서 고친다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

2. 정답 3
상품명은 복합후보키 전체가 아니라 상품id에만 의존한다. 상품표로 분리한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

3. 정답 2
공구의 참여는 선택, 대여의 부모 참조는 필수다. 부모가 자식0개인 것과 FK NULL을 혼동하지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

4. 정답 1
인조키의 유일성과 업무 후보키의 유일성은 별개다. 이메일 유일성이 실제 업무규칙이라는 전제를 먼저 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

5. 정답 4
정한 도메인에서 여러 품목을 한 칸에 넣지 않고 거래id·품목id로 분해한다. 단순 구분자 변경은 구조를 고치지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

6. 정답 3
저장·접근 구조 변경을 외부 조회 계약과 분리한 예다. 실제 성능 개선은 측정해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

7. 정답 2
C는 후보키에 속한 주요 속성이라3NF 조건은 가능하지만, BCNF는 모든 비자명 종속의 결정자가 슈퍼키여야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

8. 정답 1
슈퍼키 중 불필요한 속성을 뺄 수 없는 것이 후보키다. 표시 길이나 타입과 다르다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

9. 정답 4
부모 참조와 필수값만 보장한다. 여러 행의 좌석합은 별도 업무규칙이다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

10. 정답 3
중복 저장은 갱신 책임을 만든다. 원본 재계산과 대조하고 성능 이득·쓰기 비용을 측정해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

11. 정답 2
COUNT(*)는 NULL행도 포함하고 COUNT(v)는 v가 있는 네 행만 센다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

12. 정답 1
비NULL 네 값의 합을4로 나눈다. NULL을0으로 채운 평균과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

13. 정답 4
NULL과 =로 비교한 결과는 UNKNOWN이다. WHERE는 TRUE만 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

14. 정답 3
IS NULL은 결측을 직접 검사한다. id4만 결측이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

15. 정답 2
BETWEEN은 양끝을 포함한다. 범위 밖과 NULL은 남지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

16. 정답 1
AND가 OR보다 먼저 묶인다. A는 모두, B는 ok만 통과하지만 B에는 ok가 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

17. 정답 4
1 이외의 값도 NULL과 다르다는 비교가 UNKNOWN이다. NULL을 제외하거나 NOT EXISTS로 고친다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

18. 정답 3
CASE는 처음 TRUE가 되는 분기를 택한다. NULL을 먼저 별도로 표시한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

19. 정답 2
NULLIF는20과 같을 때만 NULL로 바꾼다. 원본 NULL은 유지되고 COALESCE만 표시0을 만든다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

20. 정답 1
A 두 행, B 두 행, C 한 행이다. B의 합은 결측을 빼고 계산하지만 COUNT(*)는2다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

21. 정답 4
HAVING은 그룹합이 만들어진 뒤 필터한다. WHERE로 SUM 조건을 옮길 수 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

22. 정답 3
부모가 있는2·3·4만 연결된다. 자기조인은 같은 표를 역할별 별칭으로 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

23. 정답 2
부모 없는1·5도 LEFT라 남는다. 부모열만 NULL로 확장된다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

24. 정답 1
왼쪽2행과 오른쪽3행의 모든 짝은2×3=6개다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

25. 정답 4
자식이 둘인1도 한 행이다. EXISTS는 바깥 행마다 존재 여부만 판정한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

26. 정답 3
그룹별 최대와 (그룹,값) 쌍을 비교한다. NULL행은 최대값의 대표가 아니다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

27. 정답 2
v만 선택했으므로20 또는15의 중복이 제거된다. id까지 선택하면 다른 결과다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

28. 정답 1
UNION ALL은 중복까지 보존해3+1=4행이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

29. 정답 4
A와 B 모두에서 나타난 값만 하나씩 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

30. 정답 3
A에만 있는 값이다. Oracle19c의 같은 목적 구문은 MINUS다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

31. 정답 2
동점 다음 RANK는 건너뛰고 DENSE_RANK는 서로 다른 값의 순서다. NULL은 계산 전에 제외했다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

32. 정답 1
id 순서로 누적하며4의 NULL을0으로 표시했다. 이 표시정책이 실제 값0을 증명하지는 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

33. 정답 4
이전/다음 행으로 이동한다. 경계와 원자료 NULL 모두 NULL 결과를 만들므로 행 위치와 같이 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

34. 정답 3
프레임을 끝까지 펼쳤으므로 모든 행의 LAST_VALUE가 id5의 값이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

35. 정답 2
정확히2행이며 동점은id 오름차순으로 푼다. 동점 모두 포함하는 등수 조건과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

36. 정답 1
뿌리1→자식2·3→손자4 순서다. 이 입력은 순환이 없으며 깊이제한만으로 일반 트리 정제를 보장하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

37. 정답 4
조건별 합계를 가로로 펼친다. 중복 원행과 순서는 집계 과정에서 사라진다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

38. 정답 3
대문자화·1기반 세번째부터2글자·하이픈 제거·소수둘째자리 반올림을 각각 계산한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

39. 정답 2
2024는 윤년,2025는 평년이다. 숫자문자는 명시적으로 변환한다. SQLite 날짜함수를 Oracle에 그대로 복사하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

40. 정답 1
같은 그룹 합을 분모로 쓴다. 정수 나눗셈을 피하려고1.0을 곱한다. 0합이면 NULLIF 등의 정책을 추가한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

41. 정답 4
NVL2는 x의 NULL 여부에 따라 두 번째/세 번째 인수를 고른다. SQLite는 CASE WHEN x IS NOT NULL로 목적을 재현한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

42. 정답 3
ROWNUM으로 먼저 고른 행만 정렬할 수 있다. 정확한 순서·동점정책을 선언해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

43. 정답 2
왼쪽 접두 열을 차례로 유지하고 마지막은 총계다. 채널만의 소계는 없다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

44. 정답 1
값이 비었는지가 아니라 그 열이 해당 집계수준에서 제외되었는지를 표시한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

45. 정답 4
부모 id와 자식 parent_id를 연결한다. LEVEL은 시작행1부터 세는 깊이다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

46. 정답 3
REGEXP_LIKE의 경계·자리수·대소문자 정책을 따로 검사해야 한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

47. 정답 2
저장점 뒤만 취소한다. 전체 ROLLBACK과 다르며 필요하면 RELEASE 후 COMMIT/ROLLBACK을 명시한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

48. 정답 1
Oracle 일반표 TRUNCATE는 전행 절단 DDL이며 되돌리기용 거래에 넣으면 안 된다. SQLite는 같은 구문이 없다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

49. 정답 4
단순 키기반 예제 결과를 비교할 수 있지만 임의 ON조건과 관리 동작까지 전부 등가라고 할 수 없다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

50. 정답 3
회수는 해당 부여 경로를 끊는다. 역할·직접권한·다른 경로를 검증해야 하며 SQLite 정책표는 실제권한 집행이 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

모의시험 전용 전체 DDL·합성 seed

CREATE TABLE t(id INTEGER PRIMARY KEY,grp TEXT,v INTEGER,parent INTEGER,flag TEXT);
INSERT INTO t VALUES (1,'A',10,NULL,'ok'),(2,'A',20,1,'ok'),(3,'B',20,1,'no'),(4,'B',NULL,2,NULL),(5,'C',5,NULL,'ok');

30 계산문항 독립 고정 정답

[
  {
    "id": "sql-exam-one-11",
    "sql": "SELECT COUNT(*),COUNT(v) FROM t",
    "expected": [
      [
        5,
        4
      ]
    ],
    "reason": "COUNT(*)는 NULL행도 포함하고 COUNT(v)는 v가 있는 네 행만 센다."
  },
  {
    "id": "sql-exam-one-12",
    "sql": "SELECT SUM(v),AVG(v) FROM t",
    "expected": [
      [
        55,
        13.75
      ]
    ],
    "reason": "비NULL 네 값의 합을4로 나눈다. NULL을0으로 채운 평균과 다르다."
  },
  {
    "id": "sql-exam-one-13",
    "sql": "SELECT id FROM t WHERE v=NULL ORDER BY id",
    "expected": [],
    "reason": "NULL과 =로 비교한 결과는 UNKNOWN이다. WHERE는 TRUE만 남긴다."
  },
  {
    "id": "sql-exam-one-14",
    "sql": "SELECT id FROM t WHERE v IS NULL ORDER BY id",
    "expected": [
      [
        4
      ]
    ],
    "reason": "IS NULL은 결측을 직접 검사한다. id4만 결측이다."
  },
  {
    "id": "sql-exam-one-15",
    "sql": "SELECT id FROM t WHERE v BETWEEN 10 AND 20 ORDER BY id",
    "expected": [
      [
        1
      ],
      [
        2
      ],
      [
        3
      ]
    ],
    "reason": "BETWEEN은 양끝을 포함한다. 범위 밖과 NULL은 남지 않는다."
  },
  {
    "id": "sql-exam-one-16",
    "sql": "SELECT id FROM t WHERE grp='A' OR grp='B' AND flag='ok' ORDER BY id",
    "expected": [
      [
        1
      ],
      [
        2
      ]
    ],
    "reason": "AND가 OR보다 먼저 묶인다. A는 모두, B는 ok만 통과하지만 B에는 ok가 없다."
  },
  {
    "id": "sql-exam-one-17",
    "sql": "SELECT id FROM t WHERE id NOT IN (1,NULL) ORDER BY id",
    "expected": [],
    "reason": "1 이외의 값도 NULL과 다르다는 비교가 UNKNOWN이다. NULL을 제외하거나 NOT EXISTS로 고친다."
  },
  {
    "id": "sql-exam-one-18",
    "sql": "SELECT id,CASE WHEN v IS NULL THEN 'missing' WHEN v>=20 THEN 'high' ELSE 'low' END FROM t ORDER BY id",
    "expected": [
      [
        1,
        "low"
      ],
      [
        2,
        "high"
      ],
      [
        3,
        "high"
      ],
      [
        4,
        "missing"
      ],
      [
        5,
        "low"
      ]
    ],
    "reason": "CASE는 처음 TRUE가 되는 분기를 택한다. NULL을 먼저 별도로 표시한다."
  },
  {
    "id": "sql-exam-one-19",
    "sql": "SELECT id,COALESCE(v,0),NULLIF(v,20) FROM t WHERE id IN (3,4) ORDER BY id",
    "expected": [
      [
        3,
        20,
        null
      ],
      [
        4,
        0,
        null
      ]
    ],
    "reason": "NULLIF는20과 같을 때만 NULL로 바꾼다. 원본 NULL은 유지되고 COALESCE만 표시0을 만든다."
  },
  {
    "id": "sql-exam-one-20",
    "sql": "SELECT grp,COUNT(*),SUM(v) FROM t GROUP BY grp ORDER BY grp",
    "expected": [
      [
        "A",
        2,
        30
      ],
      [
        "B",
        2,
        20
      ],
      [
        "C",
        1,
        5
      ]
    ],
    "reason": "A 두 행, B 두 행, C 한 행이다. B의 합은 결측을 빼고 계산하지만 COUNT(*)는2다."
  },
  {
    "id": "sql-exam-one-21",
    "sql": "SELECT grp,SUM(v) FROM t GROUP BY grp HAVING SUM(v)>=20 ORDER BY grp",
    "expected": [
      [
        "A",
        30
      ],
      [
        "B",
        20
      ]
    ],
    "reason": "HAVING은 그룹합이 만들어진 뒤 필터한다. WHERE로 SUM 조건을 옮길 수 없다."
  },
  {
    "id": "sql-exam-one-22",
    "sql": "SELECT c.id,p.id FROM t c JOIN t p ON c.parent=p.id ORDER BY c.id",
    "expected": [
      [
        2,
        1
      ],
      [
        3,
        1
      ],
      [
        4,
        2
      ]
    ],
    "reason": "부모가 있는2·3·4만 연결된다. 자기조인은 같은 표를 역할별 별칭으로 읽는다."
  },
  {
    "id": "sql-exam-one-23",
    "sql": "SELECT c.id,p.id FROM t c LEFT JOIN t p ON c.parent=p.id WHERE c.id IN (1,4,5) ORDER BY c.id",
    "expected": [
      [
        1,
        null
      ],
      [
        4,
        2
      ],
      [
        5,
        null
      ]
    ],
    "reason": "부모 없는1·5도 LEFT라 남는다. 부모열만 NULL로 확장된다."
  },
  {
    "id": "sql-exam-one-24",
    "sql": "SELECT COUNT(*) FROM t a CROSS JOIN t b WHERE a.id<=2 AND b.id<=3",
    "expected": [
      [
        6
      ]
    ],
    "reason": "왼쪽2행과 오른쪽3행의 모든 짝은2×3=6개다."
  },
  {
    "id": "sql-exam-one-25",
    "sql": "SELECT p.id FROM t p WHERE EXISTS (SELECT 1 FROM t c WHERE c.parent=p.id) ORDER BY p.id",
    "expected": [
      [
        1
      ],
      [
        2
      ]
    ],
    "reason": "자식이 둘인1도 한 행이다. EXISTS는 바깥 행마다 존재 여부만 판정한다."
  },
  {
    "id": "sql-exam-one-26",
    "sql": "SELECT id FROM t WHERE (grp,v) IN (SELECT grp,MAX(v) FROM t GROUP BY grp) ORDER BY id",
    "expected": [
      [
        2
      ],
      [
        3
      ],
      [
        5
      ]
    ],
    "reason": "그룹별 최대와 (그룹,값) 쌍을 비교한다. NULL행은 최대값의 대표가 아니다."
  },
  {
    "id": "sql-exam-one-27",
    "sql": "SELECT v FROM t WHERE id<=3 UNION SELECT v FROM t WHERE id=5 ORDER BY v",
    "expected": [
      [
        5
      ],
      [
        10
      ],
      [
        20
      ]
    ],
    "reason": "v만 선택했으므로20 또는15의 중복이 제거된다. id까지 선택하면 다른 결과다."
  },
  {
    "id": "sql-exam-one-28",
    "sql": "SELECT COUNT(*) FROM (SELECT v FROM t WHERE id<=3 UNION ALL SELECT v FROM t WHERE id=5)",
    "expected": [
      [
        4
      ]
    ],
    "reason": "UNION ALL은 중복까지 보존해3+1=4행이다."
  },
  {
    "id": "sql-exam-one-29",
    "sql": "SELECT v FROM t WHERE grp='A' INTERSECT SELECT v FROM t WHERE grp='B' ORDER BY v",
    "expected": [
      [
        20
      ]
    ],
    "reason": "A와 B 모두에서 나타난 값만 하나씩 남긴다."
  },
  {
    "id": "sql-exam-one-30",
    "sql": "SELECT v FROM t WHERE grp='A' EXCEPT SELECT v FROM t WHERE grp='B' ORDER BY v",
    "expected": [
      [
        10
      ]
    ],
    "reason": "A에만 있는 값이다. Oracle19c의 같은 목적 구문은 MINUS다."
  },
  {
    "id": "sql-exam-one-31",
    "sql": "SELECT id,RANK() OVER(ORDER BY v DESC),DENSE_RANK() OVER(ORDER BY v DESC) FROM t WHERE v IS NOT NULL ORDER BY id",
    "expected": [
      [
        1,
        3,
        2
      ],
      [
        2,
        1,
        1
      ],
      [
        3,
        1,
        1
      ],
      [
        5,
        4,
        3
      ]
    ],
    "reason": "동점 다음 RANK는 건너뛰고 DENSE_RANK는 서로 다른 값의 순서다. NULL은 계산 전에 제외했다."
  },
  {
    "id": "sql-exam-one-32",
    "sql": "SELECT id,SUM(COALESCE(v,0)) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t ORDER BY id",
    "expected": [
      [
        1,
        10
      ],
      [
        2,
        30
      ],
      [
        3,
        50
      ],
      [
        4,
        50
      ],
      [
        5,
        55
      ]
    ],
    "reason": "id 순서로 누적하며4의 NULL을0으로 표시했다. 이 표시정책이 실제 값0을 증명하지는 않는다."
  },
  {
    "id": "sql-exam-one-33",
    "sql": "SELECT id,LAG(v) OVER(ORDER BY id),LEAD(v) OVER(ORDER BY id) FROM t ORDER BY id",
    "expected": [
      [
        1,
        null,
        20
      ],
      [
        2,
        10,
        20
      ],
      [
        3,
        20,
        null
      ],
      [
        4,
        20,
        5
      ],
      [
        5,
        null,
        null
      ]
    ],
    "reason": "이전/다음 행으로 이동한다. 경계와 원자료 NULL 모두 NULL 결과를 만들므로 행 위치와 같이 읽는다."
  },
  {
    "id": "sql-exam-one-34",
    "sql": "SELECT id,LAST_VALUE(v) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t ORDER BY id",
    "expected": [
      [
        1,
        5
      ],
      [
        2,
        5
      ],
      [
        3,
        5
      ],
      [
        4,
        5
      ],
      [
        5,
        5
      ]
    ],
    "reason": "프레임을 끝까지 펼쳤으므로 모든 행의 LAST_VALUE가 id5의 값이다."
  },
  {
    "id": "sql-exam-one-35",
    "sql": "SELECT id,v FROM t WHERE v IS NOT NULL ORDER BY v DESC,id LIMIT 2",
    "expected": [
      [
        2,
        20
      ],
      [
        3,
        20
      ]
    ],
    "reason": "정확히2행이며 동점은id 오름차순으로 푼다. 동점 모두 포함하는 등수 조건과 다르다."
  },
  {
    "id": "sql-exam-one-36",
    "sql": "WITH RECURSIVE r(id,depth) AS (SELECT id,1 FROM t WHERE id=1 UNION ALL SELECT t.id,r.depth+1 FROM t JOIN r ON t.parent=r.id WHERE r.depth<5) SELECT * FROM r ORDER BY depth,id",
    "expected": [
      [
        1,
        1
      ],
      [
        2,
        2
      ],
      [
        3,
        2
      ],
      [
        4,
        3
      ]
    ],
    "reason": "뿌리1→자식2·3→손자4 순서다. 이 입력은 순환이 없으며 깊이제한만으로 일반 트리 정제를 보장하지 않는다."
  },
  {
    "id": "sql-exam-one-37",
    "sql": "SELECT SUM(CASE WHEN grp='A' THEN v END),SUM(CASE WHEN grp='B' THEN v END),SUM(CASE WHEN grp='C' THEN v END) FROM t",
    "expected": [
      [
        30,
        20,
        5
      ]
    ],
    "reason": "조건별 합계를 가로로 펼친다. 중복 원행과 순서는 집계 과정에서 사라진다."
  },
  {
    "id": "sql-exam-one-38",
    "sql": "SELECT UPPER('ab'),SUBSTR('A-12',3,2),REPLACE('A-12','-',''),ROUND(2.346,2)",
    "expected": [
      [
        "AB",
        "12",
        "A12",
        2.35
      ]
    ],
    "reason": "대문자화·1기반 세번째부터2글자·하이픈 제거·소수둘째자리 반올림을 각각 계산한다."
  },
  {
    "id": "sql-exam-one-39",
    "sql": "SELECT date('2024-02-28','+1 day'),date('2025-02-28','+1 day'),CAST('12' AS INTEGER)+1",
    "expected": [
      [
        "2024-02-29",
        "2025-03-01",
        13
      ]
    ],
    "reason": "2024는 윤년,2025는 평년이다. 숫자문자는 명시적으로 변환한다. SQLite 날짜함수를 Oracle에 그대로 복사하지 않는다."
  },
  {
    "id": "sql-exam-one-40",
    "sql": "SELECT id,ROUND(1.0*v/SUM(v) OVER(PARTITION BY grp),3) FROM t WHERE grp='A' ORDER BY id",
    "expected": [
      [
        1,
        0.333
      ],
      [
        2,
        0.667
      ]
    ],
    "reason": "같은 그룹 합을 분모로 쓴다. 정수 나눗셈을 피하려고1.0을 곱한다. 0합이면 NULLIF 등의 정책을 추가한다."
  }
]

메모리 SQLite 실제 검산

import json,sqlite3
from pathlib import Path
P=Path(__file__).resolve().parent
cases=json.loads((P/'exam-cases.json').read_text(encoding='utf-8'))
for case in cases:
 db=sqlite3.connect(':memory:')
 db.executescript((P/'exam-seed.sqlite.sql').read_text(encoding='utf-8'))
 got=[list(x) for x in db.execute(case['sql'])]
 assert got==case['expected'],(case['id'],got,case['expected'])
 db.close()
print('PASS 30 SQL exam cases; SQLite only; Oracle NOT RUN')

검산 예상 stdout

PASS 30 SQL exam cases; SQLite only; Oracle NOT RUN

자체 답안 채점·경계 자가시험

import json,sys
from pathlib import Path
P=Path(__file__).resolve().parent
key=json.loads((P/'exam-key.json').read_text(encoding='utf-8'))
def score(a):
 if len(a)!=50 or any(type(v) is not int or v not in range(5) for v in a):
  raise ValueError('답안은 50개 정수: 미응답0 또는 선택지1~4')
 flags=[x==y for x,y in zip(a,key)]
 model=2*sum(flags[:10]);sql=2*sum(flags[10:]);total=model+sql
 return model,sql,total,total>=60 and model>=8 and sql>=32
if '--self-test' in sys.argv:
 assert score(key)==(20,80,100,True)
 assert score([0]*50)==(0,0,0,False)
 assert score([0]*10+key[10:])==(0,80,80,False)
 assert score(key[:10]+[0]*40)==(20,0,20,False)
 assert score(key[:4]+[0]*6+key[10:36]+[0]*14)==(8,52,60,True)
 try:score([True]*50)
 except ValueError:pass
 else:raise AssertionError('bool answers must be rejected')
 print('PASS exam grading boundaries; learning mastery NOT certified')
else:
 a=json.loads((P/'exam-response.json').read_text(encoding='utf-8'))
 m,s,t,ok=score(a)
 print(f'model={m}/20 sql={s}/80 total={t}/100 diagnostic_pass={ok}')
 print('독립 실기·오류복구·전이평가는 별도; 공식시험 합격/숙련 인증 아님')

채점 기준 선택지

[4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3]

학습자 답안: 미응답0을 선택지1~4로 변경

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

빈답안 채점 예상

model=0/20 sql=0/80 total=0/100 diagnostic_pass=False
독립 실기·오류복구·전이평가는 별도; 공식시험 합격/숙련 인증 아님
실습 해설 보기

A=(50,2,2). B=(20,기존1,요청4,정원4,거절)이며 booking은5건 유지한다. C=전체5건/금액 있는4건/미확인1건/105 표시금액0. D=LEFT JOIN6행,40의 COUNT(*)=1/COUNT(b.id)=0. B의 요청이3이면 합4=정원4라 허용한다. 실제 INSERT는 하지 않았고 동시성 안전 보장은 별도다. 모델 답: class의 PK는id50, category_id2는FK다. booking106/107은각자PK이며 class_id50을FK로 참조한다. 강좌명을복사하면명칭갱신이여러접수행으로퍼지므로 class 한행에서관리한다. 50문항의 문항별 정답·해설은 이 단원의 exam-answers.txt에 모두 제공된다. 계산문항의 고정 정답은 exam-cases.json과 exam-verify.py로 실제 대조한다.

평가 기준: A~D 각각 정확값·중간행·원인 설명·변형 검증 네 칸을 모두 충족해야 통과한다. 하나라도 빠지면 해당 유형만 복습하고 새 입력으로 재시험한다. 공식시험 합격이나 전과목 숙련으로 판정하지 않는다. A의 PK/FK와 강좌명 갱신 이상 설명도 필수다. 모의시험은 문항당2점으로 별도 채점하고, 오답마다 중간값/규칙 오류와 입력변형 재시험 근거가 있어야 보완 완료다. 실기 미통과를 높은 객관식 점수로 대체하지 않는다.

새 맥락에 적용하기

합성 체육강좌에 정원·완료·취소·NULL을 섞어 같은 네 과제를 독립 작성하라. 새 키와 수치, 오답노트, 정원 경계 반례가 있어야 통과한다.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “키→중간행→조건→합계→반례 순서로 새 문제를 검산한다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

  • https://www.dataq.or.kr/www/sub/a_04.do — 2026-10-05 KST 공식시험50문항·90분과자체진단분리; 과목·배점·합격·과락 직접조회.
  • https://docs.python.org/3/using/index.html — 환경 준비 안내의 공식 플랫폼별 설치/인터프리터 실행 문서 실제 조회. Windows Install Manager와 macOS .pkg 경로를 추가 확인했다. 이 제작 중 신규 설치는 하지 않았다.

학습 단원

새 입력에서 다시 풀기 — 자체 진단2

해결할 문제와 목표

순위·그룹·계층·관리 구문을 따로 알아도 통합 문제에서 잘못 선택한다.

독립 고급 과제 네 개를 풀고 오답 유형을 재점검하며 시험 전 공식 안내 확인표를 작성한다.

먼저 알아둘 것

  1. 처음 본 표에도 답하기 — 자체 진단1의 예측·실행·오류복구 실습

핵심 한 문장: 정답을 외우지 말고 입력을 바꿔도 같은 원리로 풀 수 있는지 확인한다.

지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여

큰 그림: 비유와 경계

한 번 조립한 도구를 다른 작업대에서 다시 조립한다.

비유가 닿지 않는 곳: 작은 통합 과제 통과가 공식시험 성적이나 운영 DB 안전을 보장하지 않는다.

실제로 작동하는 원리

자체 진단2의 네 과제를 해설 없이 수행한다. A. 105를 완료2석으로 바꾼 뒤 강좌별 완료 좌석합에서 정확히 상위2행과 동점 포함1등을 각각 구하라. B. 음악 분류4의 자식5(합창)를 추가하고 기존 음악 소속 강좌의 완료 좌석합을 계산하라. 분류 트리와 강좌 소속은 다른 관계이므로 자식 분류가 늘어도 class30.category_id=4는 바뀌지 않는다. C. 거래 안에서102를 완료로 바꿨다가 ROLLBACK TO로 취소 상태와 완료 총좌석을 복구하라. D. 특정 접수 삭제·전체 내용 비우기·표 자체 제거·조회 권한 회수에 맞는 구문과 Oracle/SQLite 차이를 서술하라. 기본 완료 합은10=2,20=1,30=3,40=0이다. A의 입력변형으로20=3이 되어30과 동점이다. 정확히 상위2행은 합 DESC,id로20/30, 동점 포함1등도20/30이다. RANK와 ROW_NUMBER 조건의 차이를 설명한다. B의 음악 직접 자식은1개, 음악 소속 강좌의 완료 좌석합은3이다. C는 임시 합7에서 복구 후6이며102가 취소 상태로 돌아온다. D는 DELETE WHERE, 일반 Oracle 표의 TRUNCATE(롤백 불가), DROP, REVOKE SELECT다. 권한 회수는 다른 허용 경로도 검사한다. 오답을 그룹 입력·동점 정책·프레임·부모 방향·저장점 범위·방언/권한으로 분류한다. 첫 오류 절과 고친 이유를 적고, 다른 세션에서 새 입력으로 정답을 가린 재시험을 한다. 시험 시간 전략은 오래 걸리는 문제를 표시해 두고 나중에 다시 푸는 것이다. 이 네 과제는 50문항 모의시험2회차를 대신하지 않는다. 공식 확인 기준은2과목50문항90분, 총점60점 이상이면서 각 과목40% 이상이다. 회차 날짜·접수 마감·수험표·허용 신분증·입실 시간은 공식 해당 회차 공지의 URL·확인일·값을 직접 기록해야 한다. 이 교재에서는 해당 회차 세칙이 미확인이다. 추정값으로 채우지 않는다. 이 단원에는 A~D 실기와 구별되는 자체 모의 50문항도 내장되어 있다. exam-paper.txt를 먼저 열어90분 동안 풀이한다. 1~10은 모델링,11~50은SQL이며 문항당2점이다. 총60점 이상·모델링8/20점 이상·SQL32/80점 이상은 자체 진단기준일 뿐 공식 합격이나 숙련 인증이 아니다. 두 회차는 계산문항의 입력을 바꾸고 개념/관리 문항을 달리한 병렬형으로, 동일 원리를 새 입력에 적용하는지 점검한다.

따라 하기

  1. START-HERE.txt의 환경 준비와 파일 저장 절차부터 따른다. 이 단원의 모든 자료를 같은 새 폴더에 UTF-8로 저장하고 python3 environment-check.py를 실행해 ENVIRONMENT OK를 확인한다. Windows에서는 python 명령을 사용할 수 있다. 별도 Python 패키지·인증·유료 API는 필요 없다.
  2. 먼저 queries.sqlite.sql의 SELECT를 읽고 중간 행과 최종 행수를 종이에 예측한다. 해설과 tests.json을 보기 전 독립 과제를 작성한다.
  3. python3 run.py의 stdout을 expected.txt와 비교한다. PASS는 교재 예제 재현일 뿐 학습자의 독립 숙련이나 Oracle 실행 성공이 아니다.
  4. 입력변형 case를 원본 case와 비교한다. 실패하면 해당 case SQL·seed·NULL·키·정렬부터 확인하고 새 프로세스로 다시 실행한다.
  5. 시험은 exam-paper.txt와 각 문제의 독립 입력만 보고 먼저 풀이한다. 학습자가 타이머로90분을 잰다. 답안을 exam-response.json의0 대신 선택지1~4로 입력한 뒤 python3 exam-grade.py로 자체 채점한다. 미응답은0 그대로 둔다.
  6. 제출 뒤 exam-answers.txt로 문항별 이유를 확인하고 python3 exam-verify.py로30개 계산문항을 실제 SQLite 검산한다. exam-cases.json은 공개 해설용 정답이므로 시험 중에는 가린다. python3 exam-grade.py --self-test는 채점 경계를 점검한다.
  7. 틀린 문항마다 원인과 고친 SQL/모델을 error-notes.md에 적는다. 입력 한 값을 직접 바꾸어 예측→실행→중간값 대조를 다시 한다. 자동 객관식 점수와 A~D 실기 rubric은 합치지 않는다.

입력 예제

UPDATE booking SET status='완료',seats=2 WHERE id=105; WITH sums AS(SELECT c.id,COALESCE(SUM(b.seats),0) n FROM class c LEFT JOIN booking b ON b.class_id=c.id AND b.status='완료' GROUP BY c.id) SELECT id,n FROM sums ORDER BY n DESC,id LIMIT 2; WITH sums AS(SELECT class_id,SUM(seats) n FROM booking WHERE status='완료' GROUP BY class_id), ranked AS(SELECT *,DENSE_RANK() OVER(ORDER BY n DESC) dr FROM sums) SELECT class_id,n FROM ranked WHERE dr=1 ORDER BY class_id;

예상 결과

[[[20, 3], [30, 3]], [[20, 3], [30, 3]]] (바깥 배열은 SELECT 결과집합, 안쪽은 행; null은 SQL NULL)

검증 방법

  1. 모든 case의 고정 정답과 실제 결과를 assert로 대조한다. 예상한 오류는 ERROR 유형으로 기록되고 이후 SELECT로 상태를 재검사한다.
  2. A의 동점 정책, B의 관계 방향, C의 저장점 복구, D의 방언·권한을 각각 결과·중간 근거·오류 원인·변형 검증으로 평가한다. 모든 항목 충족 시 소규모 진단 통과다. 부족한 유형만 복습하며 객관식 점수와 독립 숙련은 따로 기록한다.

흔한 오해와 실패 복구

  1. 상위 N행과 상위 N등은 다르다.
  2. 모의시험 정답 기억과 새 SQL 작성 역량은 분리해 평가한다.

확인 문제

105의 완료 좌석을2에서1로 줄이면1등에20이 남는가? 이유는 무엇인가?

자기평가 · 자동채점 아님

평가 기준: 20의 합2,30의 합3, 동점 해제 후1등30 한 행을 설명하면 통과.

해설 보기

아니다.20의 합은 기존1+새1=2로30의3보다 작아져1등은30만 남는다.

도서 사본번호가 주키이고 ISBN→도서명이다. 사본마다 도서명이 다르게 바뀌는 문제를 고치려면?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

사본번호→ISBN→도서명의 이행종속을 나누고 FK로 연결한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 도서(ISBN,도서명)와 사본(사본번호,ISBN)을 분리

수강(학생,과목,성적)에서 후보키는(학생,과목)다. 학생 이름을 같은 표에 추가할 때 문제는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

학생→이름은 전체 복합키 일부에 대한 종속이다. 학생 표로 분리해야 반복 갱신을 줄인다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 학생에만 의존하므로 부분종속이 생긴다

주문이 없는 상품도 보관하고 주문행은 상품 하나를 필수 참조한다. 맞는 설계는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

판매 전 상품도 존재한다. 자식0개 허용과 자식의 부모 필수참조가 동시에 성립한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 상품1 : 주문행0..N, 주문행 상품FK NOT NULL

접수 주키를(class_id,local_no)로 정했다. class_id가 부모에서 온 키인 이 관계는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

외부 식별자가 자식 주키의 일부다. 대응수는 별도이며 이 선언만으로1:1이 되지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 부모 식별자를 자식 주키에 포함하는 식별 관계

지점 열이 '동부|서부'라는 목록이다. 지점별 담당자를 관리하려면?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

열의 업무 도메인을 원자적인 지점으로 정의한 뒤 반복 그룹을 분리한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 업무-지점 연결표를 한 지점씩 분해

기본표 열 이름을 바꿨지만 기존 앱에 옛 이름의 호환 뷰를 유지했다. 목표는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

논리 구조 변경과 외부 조회계약을 분리하는 설계다. 호환성이 자동으로 생기는 것은 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 논리적 데이터 독립성

SC→I, I→C인 수강표를 IC와 SI로 분해했다. 중요한 점은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

공통 I가 IC를 결정하므로 무손실이지만, 한 학생이 같은 과목의 두 강사를 갖는 모순은 JOIN을 보아야 검출할 수 있다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 무손실이어도 SC→I는 각 표 제약만으로 보존되지 않을 수 있다

UNIQUE(a,b)인데 a만 빼도 b가 항상 유일하다. (a,b)를 최소 후보키라 할 수 있나?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

최소성은 속성을 제거해도 유일성이 유지되는지로 판정한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 아니다. b만으로 충분하여 불필요한 a가 있다

CHECK(qty>=0)만 있는 수량열에서 NULL을 막고 싶다. 무엇을 추가하나?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

CHECK는 FALSE를 거부하지만 UNKNOWN은 허용될 수 있다. 필수값 제약이 별도로 필요하다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: NOT NULL

고객 주소 요약을 별도 표에 복제했다. 정규화 포기 여부의 판단 근거는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

반정규화는 성능 가설이다. 읽기/쓰기 부하, 갱신 주기, 정합성 복구를 측정하고 책임을 명시한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 실측 읽기 이득과 갱신·불일치 비용 비교

자체 모의 2회 11번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT COUNT(*),COUNT(v) FROM t;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

COUNT(*)는 NULL행도 포함하고 COUNT(v)는 v가 있는 네 행만 센다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[5,4]]

자체 모의 2회 12번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT SUM(v),AVG(v) FROM t;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

비NULL 네 값의 합을4로 나눈다. NULL을0으로 채운 평균과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[65,16.25]]

자체 모의 2회 13번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE v=NULL ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

NULL과 =로 비교한 결과는 UNKNOWN이다. WHERE는 TRUE만 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: []

자체 모의 2회 14번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE v IS NULL ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

IS NULL은 결측을 직접 검사한다. id4만 결측이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[4]]

자체 모의 2회 15번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE v BETWEEN 10 AND 20 ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

BETWEEN은 양끝을 포함한다. 범위 밖과 NULL은 남지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[2],[3]]

자체 모의 2회 16번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE grp='A' OR grp='B' AND flag='ok' ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

AND가 OR보다 먼저 묶인다. A는 모두, B는 ok만 통과하지만 B에는 ok가 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1],[2]]

자체 모의 2회 17번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE id NOT IN (1,NULL) ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

1 이외의 값도 NULL과 다르다는 비교가 UNKNOWN이다. NULL을 제외하거나 NOT EXISTS로 고친다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: []

자체 모의 2회 18번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,CASE WHEN v IS NULL THEN 'missing' WHEN v>=20 THEN 'high' ELSE 'low' END FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

CASE는 처음 TRUE가 되는 분기를 택한다. NULL을 먼저 별도로 표시한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,"low"],[2,"low"],[3,"low"],[4,"missing"],[5,"high"]]

자체 모의 2회 19번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,COALESCE(v,0),NULLIF(v,20) FROM t WHERE id IN (3,4) ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

NULLIF는20과 같을 때만 NULL로 바꾼다. 원본 NULL은 유지되고 COALESCE만 표시0을 만든다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[3,15,15],[4,0,null]]

자체 모의 2회 20번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT grp,COUNT(*),SUM(v) FROM t GROUP BY grp ORDER BY grp;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

A 두 행, B 두 행, C 한 행이다. B의 합은 결측을 빼고 계산하지만 COUNT(*)는2다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["A",2,20],["B",2,15],["C",1,30]]

자체 모의 2회 21번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT grp,SUM(v) FROM t GROUP BY grp HAVING SUM(v)>=20 ORDER BY grp;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

HAVING은 그룹합이 만들어진 뒤 필터한다. WHERE로 SUM 조건을 옮길 수 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["A",20],["C",30]]

자체 모의 2회 22번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT c.id,p.id FROM t c JOIN t p ON c.parent=p.id ORDER BY c.id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

부모가 있는2·3·4만 연결된다. 자기조인은 같은 표를 역할별 별칭으로 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[2,1],[3,1],[4,2]]

자체 모의 2회 23번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT c.id,p.id FROM t c LEFT JOIN t p ON c.parent=p.id WHERE c.id IN (1,4,5) ORDER BY c.id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

부모 없는1·5도 LEFT라 남는다. 부모열만 NULL로 확장된다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,null],[4,2],[5,null]]

자체 모의 2회 24번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT COUNT(*) FROM t a CROSS JOIN t b WHERE a.id<=2 AND b.id<=3;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

왼쪽2행과 오른쪽3행의 모든 짝은2×3=6개다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[6]]

자체 모의 2회 25번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT p.id FROM t p WHERE EXISTS (SELECT 1 FROM t c WHERE c.parent=p.id) ORDER BY p.id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

자식이 둘인1도 한 행이다. EXISTS는 바깥 행마다 존재 여부만 판정한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1],[2]]

자체 모의 2회 26번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id FROM t WHERE (grp,v) IN (SELECT grp,MAX(v) FROM t GROUP BY grp) ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

그룹별 최대와 (그룹,값) 쌍을 비교한다. NULL행은 최대값의 대표가 아니다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[2],[3],[5]]

자체 모의 2회 27번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT v FROM t WHERE id<=3 UNION SELECT v FROM t WHERE id=5 ORDER BY v;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

v만 선택했으므로20 또는15의 중복이 제거된다. id까지 선택하면 다른 결과다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[5],[15],[30]]

자체 모의 2회 28번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT COUNT(*) FROM (SELECT v FROM t WHERE id<=3 UNION ALL SELECT v FROM t WHERE id=5);

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

UNION ALL은 중복까지 보존해3+1=4행이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[4]]

자체 모의 2회 29번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT v FROM t WHERE grp='A' INTERSECT SELECT v FROM t WHERE grp='B' ORDER BY v;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

A와 B 모두에서 나타난 값만 하나씩 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[15]]

자체 모의 2회 30번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT v FROM t WHERE grp='A' EXCEPT SELECT v FROM t WHERE grp='B' ORDER BY v;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

A에만 있는 값이다. Oracle19c의 같은 목적 구문은 MINUS다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[5]]

자체 모의 2회 31번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,RANK() OVER(ORDER BY v DESC),DENSE_RANK() OVER(ORDER BY v DESC) FROM t WHERE v IS NOT NULL ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

동점 다음 RANK는 건너뛰고 DENSE_RANK는 서로 다른 값의 순서다. NULL은 계산 전에 제외했다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,4,3],[2,2,2],[3,2,2],[5,1,1]]

자체 모의 2회 32번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,SUM(COALESCE(v,0)) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

id 순서로 누적하며4의 NULL을0으로 표시했다. 이 표시정책이 실제 값0을 증명하지는 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,5],[2,20],[3,35],[4,35],[5,65]]

자체 모의 2회 33번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,LAG(v) OVER(ORDER BY id),LEAD(v) OVER(ORDER BY id) FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

이전/다음 행으로 이동한다. 경계와 원자료 NULL 모두 NULL 결과를 만들므로 행 위치와 같이 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,null,15],[2,5,15],[3,15,null],[4,15,30],[5,null,null]]

자체 모의 2회 34번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,LAST_VALUE(v) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

프레임을 끝까지 펼쳤으므로 모든 행의 LAST_VALUE가 id5의 값이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,30],[2,30],[3,30],[4,30],[5,30]]

자체 모의 2회 35번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,v FROM t WHERE v IS NOT NULL ORDER BY v DESC,id LIMIT 2;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

정확히2행이며 동점은id 오름차순으로 푼다. 동점 모두 포함하는 등수 조건과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[5,30],[2,15]]

자체 모의 2회 36번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. WITH RECURSIVE r(id,depth) AS (SELECT id,1 FROM t WHERE id=1 UNION ALL SELECT t.id,r.depth+1 FROM t JOIN r ON t.parent=r.id WHERE r.depth<5) SELECT * FROM r ORDER BY depth,id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

뿌리1→자식2·3→손자4 순서다. 이 입력은 순환이 없으며 깊이제한만으로 일반 트리 정제를 보장하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,1],[2,2],[3,2],[4,3]]

자체 모의 2회 37번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT SUM(CASE WHEN grp='A' THEN v END),SUM(CASE WHEN grp='B' THEN v END),SUM(CASE WHEN grp='C' THEN v END) FROM t;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

조건별 합계를 가로로 펼친다. 중복 원행과 순서는 집계 과정에서 사라진다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[20,15,30]]

자체 모의 2회 38번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT UPPER('ab'),SUBSTR('A-12',3,2),REPLACE('A-12','-',''),ROUND(2.346,2);

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

대문자화·1기반 세번째부터2글자·하이픈 제거·소수둘째자리 반올림을 각각 계산한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["AB","12","A12",2.35]]

자체 모의 2회 39번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT date('2024-02-28','+1 day'),date('2025-02-28','+1 day'),CAST('12' AS INTEGER)+1;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

2024는 윤년,2025는 평년이다. 숫자문자는 명시적으로 변환한다. SQLite 날짜함수를 Oracle에 그대로 복사하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [["2024-02-29","2025-03-01",13]]

자체 모의 2회 40번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL). [[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]] SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다. SELECT id,ROUND(1.0*v/SUM(v) OVER(PARTITION BY grp),3) FROM t WHERE grp='A' ORDER BY id;

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

같은 그룹 합을 분모로 쓴다. 정수 나눗셈을 피하려고1.0을 곱한다. 0합이면 NULLIF 등의 정책을 추가한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: [[1,0.25],[2,0.75]]

Oracle COALESCE(NULL,0,7)의 값은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

첫 비NULL 값이0이다. NVL/NVL2와 입력개수·타입변환 규칙이 항상 같은 것은 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 0

정확히2행 대신 금액 2등까지 동점을 모두 뽑으려면?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

서로 다른 상위2개 금액 수준을 의미한다. 정확히2행 정책과 분리하고 최종 출력에는 고유키 정렬을 더한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: DENSE_RANK를 금액만으로 계산해<=2 필터

Oracle CUBE(지역,채널)가 ROLLUP(지역,채널)에 더하는 수준은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

두 열 CUBE는 상세·지역·채널·총계의 네 그룹집합이다. 각 수준의 합을 모두 더하면 중복계산이다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 채널만의 소계

Oracle UNPIVOT 기본과 SQLite UNION ALL 세로변환의 NULL 차이는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

INCLUDE NULLS 또는 SQLite WHERE IS NOT NULL로 정책을 맞춘다. 피벗 집계에서 사라진 원행은 복구되지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 기본 UNPIVOT은 NULL 셀 제외, UNION ALL은 보존

CONNECT BY PRIOR parent_id=id는 어떤 방향인가?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

PRIOR가 가리키는 이전 행의 parent_id와 현재 행의 id를 맞춘다. 시작행을 명시하고 순환도 점검한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 자식에서 부모 방향으로 올라간다

^A-[0-9]{2}$ 패턴에 개행/대소문자 조건을 별도로 명시하는 이유는?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

ASCII두자리·길이4·개행금지·대문자라는 계약을 같이 검사한다. Python UDF를 Oracle 전체호환이라 주장하지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 엔진별 끝경계·플래그 차이로 과매칭을 막기 위해

SQLite 자동커밋으로 UPDATE한 뒤 BEGIN 없이 ROLLBACK했다. 왜 복구되지 않나?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

거래 경계 전에 확정된 변경은 롤백 대상이 아니다. 실습에선 메모리DB를 초기화하고 다시 명시거래로 시작한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 갱신이 이미 확정되었고 활성 거래가 없다

Oracle에서 DML UPDATE 후 문법적으로 유효한 DDL이 실행 오류를 냈다. 직전 DML은 안전하게 롤백 가능한가?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

Oracle은 문법적으로 유효한 DDL 직전에 커밋한다. 성공 직후 커밋도 별도다. DDL을 DML복구용 저장점 사이에 두지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: DDL 직전 암묵 COMMIT 때문에 이미 확정될 수 있다

Oracle MERGE 원본에 같은 대상키가 여러 번 나온다. 먼저 확인할 것은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

한 대상행을 여러 번 바꾸는 위험을 원본 집계/중복정책으로 해결한다. SQLite UPSERT 성공이 Oracle 성공 증거는 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 원본 매칭키 유일성과 대상의 중복 갱신 위험

CREATE ROLE 권한과 SELECT ON 표 권한의 구분은?

자동채점 · 선택형

답을 고르세요
해설과 정답 보기

역할은 권한 묶음이고 역할 생성 자체와 특정 객체 읽기는 다른 권한이다. 새 실권한은 이 실습에서 변경하지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

정답: 전자는 시스템권한, 후자는 객체권한

독립 실습

A~D를 해설 없이 직접 풀어라. 105의 새 좌석을2에서1로 바꾸어 동점 포함1등을 재시험하고, 오답노트와 응시 안내 미확인 항목을 작성하라. 별도의 exam-paper.txt 50문항도 풀고 자체 점수와 A~D 독립 수행 결과를 구분하여 제출하라. 교육자 재사용 과제: 강사용 재사용 과제: 물품 D4와 완료 R5 한 행을 추가하고 조인 중복/NULL 경계를 설명시킨다. D4=1을 수작업과 SQL로 확인하며 WHERE가 LEFT JOIN 의미를 바꾸는 이유를 질문한다. Oracle 권한 실습을 했다고 주장하지 않는다.

남길 증거: 독립 SQL, 중간행, 입력변형 결과, 오답노트와 근거. A의 동점 정책, B의 관계 방향, C의 저장점 복구, D의 방언·권한을 각각 결과·중간 근거·오류 원인·변형 검증으로 평가한다. 모든 항목 충족 시 소규모 진단 통과다. 부족한 유형만 복습하며 객관식 점수와 독립 숙련은 따로 기록한다. 50문항 답안·90분 사용기록·과목별 자체 점수·오답별 수정근거를 별도로 제출한다. 보강 증거: 예측→관측→수정 후 출력과 타인 인계/미실시 기록을 구분한다.

SQLite 독립 DDL·가상 seed

-- SQLite 3.39+; Python 표준라이브러리 sqlite3; 메모리 DB 전용
PRAGMA foreign_keys=ON;
CREATE TABLE category(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES category(id), name TEXT NOT NULL);
CREATE TABLE class(id INTEGER PRIMARY KEY, category_id INTEGER NOT NULL REFERENCES category(id), name TEXT NOT NULL, capacity INTEGER NOT NULL CHECK(capacity>0));
CREATE TABLE booking(id INTEGER PRIMARY KEY, class_id INTEGER NOT NULL REFERENCES class(id), alias TEXT NOT NULL, seats INTEGER NOT NULL CHECK(seats>0), status TEXT, paid INTEGER);
INSERT INTO category VALUES (1,NULL,'만들기'),(2,1,'목공'),(3,1,'종이'),(4,NULL,'음악');
INSERT INTO class VALUES (10,2,'나무 모빌',5),(20,3,'접는 등',4),(30,4,'리듬',6),(40,4,'합주',3);
INSERT INTO booking VALUES (101,10,'가람',2,'완료',12000),(102,10,'누리',1,'취소',12000),(103,20,'다온',1,'완료',8000),(104,30,'라온',3,'완료',15000),(105,20,'마루',1,NULL,NULL);

SQLite 실행 질의·오류·입력변형

-- SQLite only; -- case markers delimit isolated exercises
-- case: advanced-a
UPDATE booking SET status='완료',seats=2 WHERE id=105; WITH sums AS(SELECT c.id,COALESCE(SUM(b.seats),0) n FROM class c LEFT JOIN booking b ON b.class_id=c.id AND b.status='완료' GROUP BY c.id) SELECT id,n FROM sums ORDER BY n DESC,id LIMIT 2; WITH sums AS(SELECT class_id,SUM(seats) n FROM booking WHERE status='완료' GROUP BY class_id), ranked AS(SELECT *,DENSE_RANK() OVER(ORDER BY n DESC) dr FROM sums) SELECT class_id,n FROM ranked WHERE dr=1 ORDER BY class_id;
-- case: advanced-b
INSERT INTO category VALUES(5,4,'합창'); SELECT id,parent_id FROM category WHERE parent_id=4; SELECT SUM(b.seats) FROM class c JOIN booking b ON b.class_id=c.id WHERE c.category_id=4 AND b.status='완료';
-- case: advanced-c
BEGIN; SAVEPOINT s; UPDATE booking SET status='완료' WHERE id=102; SELECT SUM(seats) FROM booking WHERE status='완료'; ROLLBACK TO s; RELEASE s; SELECT SUM(seats) FROM booking WHERE status='완료'; SELECT status FROM booking WHERE id=102; COMMIT;
-- case: variant-no-tie
UPDATE booking SET status='완료',seats=1 WHERE id=105; WITH sums AS(SELECT class_id,SUM(seats) n FROM booking WHERE status='완료' GROUP BY class_id), ranked AS(SELECT *,DENSE_RANK() OVER(ORDER BY n DESC) dr FROM sums) SELECT class_id,n FROM ranked WHERE dr=1 ORDER BY class_id;

SELECT별 고정 정답과 오류 유형

{
  "advanced-a": [
    [
      [
        20,
        3
      ],
      [
        30,
        3
      ]
    ],
    [
      [
        20,
        3
      ],
      [
        30,
        3
      ]
    ]
  ],
  "advanced-b": [
    [
      [
        5,
        4
      ]
    ],
    [
      [
        3
      ]
    ]
  ],
  "advanced-c": [
    [
      [
        7
      ]
    ],
    [
      [
        6
      ]
    ],
    [
      [
        "취소"
      ]
    ]
  ],
  "variant-no-tie": [
    [
      [
        30,
        3
      ]
    ]
  ]
}

Python stdlib SQLite 메모리 실행·자동검산

from pathlib import Path
import sqlite3,json,re
P=Path(__file__).resolve().parent
if sqlite3.sqlite_version_info < (3,39,0):
    raise SystemExit('SQLite 3.39+ required for RIGHT/FULL JOIN; current='+sqlite3.sqlite_version)
def statements(text):
    buf=''
    for char in text:
        buf+=char
        if char==';' and sqlite3.complete_statement(buf):
            yield buf.strip(); buf=''
    if buf.strip():
        raise ValueError('incomplete SQL: '+buf)
def match(pattern,value):
    return None if value is None else int(re.search(pattern,value) is not None)
def extract(pattern,value):
    m=None if value is None else re.search(pattern,value)
    return None if m is None else m.group(0)
raw=(P/'queries.sqlite.sql').read_text(encoding='utf-8')
chunks=re.split(r'^-- case: ([a-z0-9-]+)\s*$',raw,flags=re.M)
tests=json.loads((P/'tests.json').read_text(encoding='utf-8'))
assert set(chunks[1::2])==set(tests)
for name,sql in zip(chunks[1::2],chunks[2::2]):
    db=sqlite3.connect(':memory:',isolation_level=None)
    db.create_function('py_match',2,match)
    db.create_function('py_extract',2,extract)
    db.create_function('py_replace',3,lambda pat,repl,s: None if s is None else re.sub(pat,repl,s))
    db.executescript((P/'setup.sqlite.sql').read_text(encoding='utf-8'))
    results=[]
    for stmt in statements(sql):
        try:
            cur=db.execute(stmt)
            if cur.description is not None:
                results.append([list(row) for row in cur.fetchall()])
        except (sqlite3.IntegrityError,sqlite3.OperationalError) as e:
            results.append('ERROR '+type(e).__name__)
    assert results==tests[name], (name,results,tests[name])
    print(name+'|'+json.dumps(results,ensure_ascii=False,separators=(',',':')))
    db.close()
print('PASS '+str(len(tests))+' cases; in-memory SQLite; Oracle NOT RUN')

독립 진단 오답노트 양식

# 자체 독립 진단 오답노트
공식 기출·공식 시험 성적 아님. 각 과제는 해설을 가리고 풀이한다.

|과제|내 SQL|예측 행|실제 행|첫 오류 절|원인|수정 SQL|새 입력|재시험 결과|
|---|---|---|---|---|---|---|---|---|
|A|||||||||
|B|||||||||
|C|||||||||
|D|||||||||

맞혔으면 원인 칸에 오류 없음과 검증 근거를 적는다.
객관식 자체 50문항 시험은 별도 연계하며 이 네 과제를 50문항으로 세지 않는다.

실제 검산한 Python stdout (SQLite; Oracle 제외)

advanced-a|[[[20,3],[30,3]],[[20,3],[30,3]]]
advanced-b|[[[5,4]],[[3]]]
advanced-c|[[[7]],[[6]],[["취소"]]]
variant-no-tie|[[[30,3]]]
PASS 4 cases; in-memory SQLite; Oracle NOT RUN

환경 준비·설치 전제·저장·실행·복구 전체 안내

처음 실행하는 학습자를 위한 준비 안내

권리·안전
  모든 입력은 독자 합성 자료다. 실명·연락처·실제 명부·비밀키를 추가하지 않는다.
  원기출 복원·원강의 복제·공인/제휴·합격 보장이 아니다. 유료 서비스나 로그인은 필요 없다.
  제작자는 새 프로그램·패키지를 설치하지 않았다. 아래 설치 설명은 학습자에게 필요한 전제 안내이며 설치 수행을 뜻하지 않는다.

1. 기존 환경을 먼저 확인한다.
  Windows 터미널: python --version
  macOS/Linux 터미널: python3 --version
  Python 3.10 이상이 필요하다. SQL 과정은 함께 제공되는 sqlite3 엔진 3.39 이상도 필요하다.
  이미 승인된 교육용 컴퓨터가 있으면 그 환경을 우선 사용한다. pip, numpy, pandas, scikit-learn, Oracle 서버 설치는 필요 없다.

2. Python이 없다면, 본인 소유 컴퓨터 또는 관리자 승인이 있는 환경에서만 준비한다.
  Windows: https://www.python.org/downloads/ 의 Python Install Manager 공식 설치 파일을 연다.
  설치 안내를 확인하고 Install을 선택한 뒤 새 터미널에서 python --version을 확인한다.
  기존 Python/PATH를 임의 삭제하지 말고 관리자와 충돌을 확인한다.
  macOS: https://www.python.org/downloads/macos/ 의 운영체제 호환 .pkg를 받아 서명·지원OS를 확인한다.
  .pkg를 열어 안내·라이선스를 읽고 승인 범위에서 설치한 뒤 새 터미널에서 python3 --version을 확인한다.
  Linux: 배포판에 이미 있는 python3를 우선 사용한다. 없다면 관리자에게 배포판 공식 Python3와 sqlite3 지원 모듈을 요청한다.
  이 교재는 sudo·소스빌드·대규모 패키지 묶음·새 유료 구독을 요구하지 않는다. 설치 권한이 없으면 중단하고 승인된 환경으로 이동한다.
  공식 확인: https://docs.python.org/3/using/windows.html , https://docs.python.org/3/using/mac.html

3. 파일을 저장한다.
  이 단원의 materials에 표시된 파일을 전부 같은 새 폴더에 저장한다. 다른 단원과 폴더를 섞지 않는다.
  파일 내용만 복사한다. 코드 울타리 기호나 화면 설명은 파일에 넣지 않는다.
  일반 텍스트 편집기로 UTF-8 저장하며 파일명.py.txt처럼 확장자가 중복되지 않게 한다.
  CSV의 첫 줄은 열 이름이며 NA 표기는 제공 파일 그대로 둔다. SQL과 R 파일을 Python 파일로 저장하지 않는다.
  Windows는 해당 폴더에서 터미널 열기, macOS/Linux는 터미널에서 cd 다음 공백을 넣고 폴더를 끌어 넣은 뒤 Enter로 이동한다.

4. 환경과 실행 위치를 검사한다.
  Windows: python environment-check.py
  macOS/Linux: python3 environment-check.py
  마지막에 ENVIRONMENT OK가 나와야 한다. 버전 번호 자체는 컴퓨터마다 달라도 최소 조건을 만족하면 된다.
  이후 본문 steps의 명령을 사용한다. Windows에서 python3가 없으면 python으로 바꾼다.
  python3 -O는 쓰지 않는다. -O는 교재의 assert 검산을 끈다.
  기본 실행의 stdout을 expected.txt와 비교하고, 변형은 variant-expected.txt 또는 SQL의 variant case와 대조한다.
  같은 값의 -0.0/0.0이나 부동소수 마지막 자리는 동치일 수 있다. 숫자 허용오차는 본문 검산식으로 판단한다.

5. 오류를 복구한다.
  command not found: 기존 Python 경로·공식 설치 상태를 확인한다. 새 유료 도구로 바꾸지 않는다.
  can't open file / FileNotFoundError: 현재 폴더와 파일명·확장자를 확인하고 해당 단원의 모든 자료를 다시 저장한다.
  No module named sqlite3: pip로 sqlite3를 설치하지 않는다. 배포판 제공 Python의 sqlite 지원을 관리자에게 확인한다.
  SQLite 버전이 낮으면 RIGHT/FULL 구문을 임의 삭제하지 말고 승인된 최신 Python 환경으로 옮긴다.
  no such table: SQL만 따로 붙여넣지 말고 전체 자료와 run.py를 사용한다. 각 case마다 setup을 새로 적용한다.
  no such function py_match: Python UDF를 등록하는 run.py를 사용한다. SQLite 내장/Oracle 함수라고 착각하지 않는다.
  IntegrityError가 의도된 사례는 ERROR 유형과 다음 SELECT 상태까지 맞아야 PASS다. 다른 오류는 무시하지 않는다.
  AssertionError: 검산을 지우지 말고 입력·중간값·정렬·NULL·정답을 비교한다. 운영DB에 재시도하지 않는다.

6. R 파일이 포함된 단원
  Rscript --version으로 기존 R을 확인한다. 제작 환경에는 Rscript가 없어 모든 R 코드는 미실행이다.
  실제 R 환경이 없으면 문서 기반 결과검토와 Python 수치대조까지 수행하고 R 실행 칸은 미실행으로 둔다.
  R 환경 준비가 허가된 학습자만 https://cran.r-project.org/bin/windows/base/ (Windows) 또는
  https://cran.r-project.org/bin/macosx/ (macOS)에서 OS/CPU와 맞는 공식 기본 배포본을 사용한다.
  설치 안내를 읽고 승인 범위에서만 설치한다. 추가 패키지·RStudio·대규모 설치는 이 교재의 요구사항이 아니다.
  Rscript가 PATH에 없다면 기존 R 콘솔에서 getwd()와 setwd(학습자료폴더)를 확인한 뒤 source("본문에서 지정한 파일.R")로 기본 예제를 읽을 수 있다.
  변형 실행은 본문 Rscript 명령을 사용한다. R 콘솔의 source는 --variant 명령행 인수를 자동 전달하지 않는다.
  CSV와 .R을 같은 현재 폴더에 놓고 Rscript 파일.R을 실행한다. r-expected-not-run.txt는 문서에서 예상한 값이지 실제 캡처가 아니다.
  PCA/MDS의 축 부호·방향은 달라도 된다. 고유값·거리·재구성 결과를 대조한다.

7. Oracle 파일이 포함된 단원
  comparison.oracle.sql은 Oracle19c 공식문법 학습·정적 대조용이고 실제 Oracle 미실행이다.
  SQLite run.py는 이 파일을 실행하지 않는다. 새 계정·서버·유료접속·실권한을 만들지 않는다.
  별도로 이미 승인된 폐기용 Oracle 환경이 있는 경우에도 파일의 빈 스키마 전제·DDL 암묵커밋·가상 역할 이름을 검토해야 한다.
  운영DB에는 붙여넣지 않는다. 예제 이름 충돌은 DROP으로 무작정 해결하지 않는다.

성취 확인
  PASS 출력은 예제 코드 검산이다. 질문의 이유·독립 입력변형·실패복구·전이과제를 직접 수행해야 단원 성취를 판단할 수 있다.
  파일은 본 단원에 모두 포함되어 있고 제작자의 개인 폴더나 비공개 파일을 찾아갈 필요가 없다.

표준라이브러리 환경 확인

import sys
from pathlib import Path
if sys.version_info < (3,10):
    raise SystemExit('Python 3.10+ required')
print('Python',sys.version.split()[0])
print('working_folder',Path.cwd().name)
if True:
    try:import sqlite3
    except ImportError:raise SystemExit('sqlite3 module missing: use an approved Python distribution with SQLite support')
    print('SQLite',sqlite3.sqlite_version)
    if sqlite3.sqlite_version_info < (3,39):raise SystemExit('SQLite 3.39+ required')
print('ENVIRONMENT OK')

자체 50문항 문제지·90분 자가진단

AX4U 독자 모의 2회 — 원기출 복원 아님 / 공식 합격 보장 없음
가상 데이터만 사용. 시험시간 90분은 학습자가 타이머로 잰다. 웹 자동 시간제한 기능을 뜻하지 않는다.
1~10: 모델링20점, 11~50: SQL80점, 문항당2점. 두 회차는 병렬형: 계산30문항의 수요값을 바꾸고 모델링·방언/관리20문항은 다른 질문을 사용한다.
총60점 이상이면서 모델링8점 이상, SQL32점 이상을 자체 진단기준으로 사용. 독립 SQL 작성과 오류복구는 기존 A~D 실기과제로 별도평가.
문제지와 seed만 먼저 열고 해설·exam-cases.json·코드는 제출 뒤 확인한다.


1. 도서 사본번호가 주키이고 ISBN→도서명이다. 사본마다 도서명이 다르게 바뀌는 문제를 고치려면?
1) 사본번호를 없앤다
2) 모든 도서명을 별칭으로 자유입력
3) 인덱스만 추가
4) 도서(ISBN,도서명)와 사본(사본번호,ISBN)을 분리

2. 수강(학생,과목,성적)에서 후보키는(학생,과목)다. 학생 이름을 같은 표에 추가할 때 문제는?
1) 복합키가 단일키가 된다
2) 행 수가 자동 줄어든다
3) 학생에만 의존하므로 부분종속이 생긴다
4) 성적이 자동 정규화된다

3. 주문이 없는 상품도 보관하고 주문행은 상품 하나를 필수 참조한다. 맞는 설계는?
1) 주문행에 없는 상품번호 허용
2) 상품1 : 주문행0..N, 주문행 상품FK NOT NULL
3) 상품은 주문행 없이 존재 불가
4) 상품과 주문행 항상1:1

4. 접수 주키를(class_id,local_no)로 정했다. class_id가 부모에서 온 키인 이 관계는?
1) 부모 식별자를 자식 주키에 포함하는 식별 관계
2) 부모가 없어도 되는 관계
3) 부모키와 무관한 비식별 관계
4) 항상1:1 관계

5. 지점 열이 '동부|서부'라는 목록이다. 지점별 담당자를 관리하려면?
1) 구분자만 쉼표로 바꾼다
2) 문자열 정렬만 한다
3) 목록 길이를 무제한으로 한다
4) 업무-지점 연결표를 한 지점씩 분해

6. 기본표 열 이름을 바꿨지만 기존 앱에 옛 이름의 호환 뷰를 유지했다. 목표는?
1) 표본 편향 해소
2) 자동 성능 보장
3) 논리적 데이터 독립성
4) 암호화 완료

7. SC→I, I→C인 수강표를 IC와 SI로 분해했다. 중요한 점은?
1) 분해하면 후보키가 사라짐
2) 무손실이어도 SC→I는 각 표 제약만으로 보존되지 않을 수 있다
3) 모든 함수종속이 자동 보존
4) JOIN이 항상 정보손실

8. UNIQUE(a,b)인데 a만 빼도 b가 항상 유일하다. (a,b)를 최소 후보키라 할 수 있나?
1) 아니다. b만으로 충분하여 불필요한 a가 있다
2) 열이 둘이면 항상 최소
3) 문자형이면 최소
4) NULL이 없으면 무조건 최소

9. CHECK(qty>=0)만 있는 수량열에서 NULL을 막고 싶다. 무엇을 추가하나?
1) ORDER BY
2) COALESCE를 보고서에만 사용
3) 별칭 AS
4) NOT NULL

10. 고객 주소 요약을 별도 표에 복제했다. 정규화 포기 여부의 판단 근거는?
1) 정규화는 언제나 성능을 낮춤
2) 인조키가 있으면 불일치 불가
3) 실측 읽기 이득과 갱신·불일치 비용 비교
4) 테이블이 적으면 언제나 빠름

11. 자체 모의 2회 11번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT COUNT(*),COUNT(v) FROM t;
1) [[null,null]]
2) [[5,4]]
3) [[5,4],[5,4]]
4) [[6,5]]

12. 자체 모의 2회 12번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT SUM(v),AVG(v) FROM t;
1) [[65,16.25]]
2) [[65,16.25],[65,16.25]]
3) [[66,17.25]]
4) [[null,null]]

13. 자체 모의 2회 13번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE v=NULL ORDER BY id;
1) [[null]]
2) [[0]]
3) [[1]]
4) []

14. 자체 모의 2회 14번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE v IS NULL ORDER BY id;
1) [[5]]
2) [[null]]
3) [[4]]
4) [[4],[4]]

15. 자체 모의 2회 15번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE v BETWEEN 10 AND 20 ORDER BY id;
1) [[null],[null]]
2) [[2],[3]]
3) [[2]]
4) [[3],[4]]

16. 자체 모의 2회 16번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE grp='A' OR grp='B' AND flag='ok' ORDER BY id;
1) [[1],[2]]
2) [[1]]
3) [[2],[3]]
4) [[null],[null]]

17. 자체 모의 2회 17번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE id NOT IN (1,NULL) ORDER BY id;
1) [[null]]
2) [[0]]
3) [[1]]
4) []

18. 자체 모의 2회 18번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,CASE WHEN v IS NULL THEN 'missing' WHEN v>=20 THEN 'high' ELSE 'low' END FROM t ORDER BY id;
1) [[2,"LOW"],[3,"LOW"],[4,"LOW"],[5,"MISSING"],[6,"HIGH"]]
2) [[null,null],[null,null],[null,null],[null,null],[null,null]]
3) [[1,"low"],[2,"low"],[3,"low"],[4,"missing"],[5,"high"]]
4) [[1,"low"],[2,"low"],[3,"low"],[4,"missing"]]

19. 자체 모의 2회 19번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,COALESCE(v,0),NULLIF(v,20) FROM t WHERE id IN (3,4) ORDER BY id;
1) [[null,null,null],[null,null,0]]
2) [[3,15,15],[4,0,null]]
3) [[3,15,15]]
4) [[4,16,16],[5,1,0]]

20. 자체 모의 2회 20번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT grp,COUNT(*),SUM(v) FROM t GROUP BY grp ORDER BY grp;
1) [["A",2,20],["B",2,15],["C",1,30]]
2) [["A",2,20],["B",2,15]]
3) [["a",3,21],["b",3,16],["c",2,31]]
4) [[null,null,null],[null,null,null],[null,null,null]]

21. 자체 모의 2회 21번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT grp,SUM(v) FROM t GROUP BY grp HAVING SUM(v)>=20 ORDER BY grp;
1) [["A",20]]
2) [["a",21],["c",31]]
3) [[null,null],[null,null]]
4) [["A",20],["C",30]]

22. 자체 모의 2회 22번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT c.id,p.id FROM t c JOIN t p ON c.parent=p.id ORDER BY c.id;
1) [[3,2],[4,2],[5,3]]
2) [[null,null],[null,null],[null,null]]
3) [[2,1],[3,1],[4,2]]
4) [[2,1],[3,1]]

23. 자체 모의 2회 23번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT c.id,p.id FROM t c LEFT JOIN t p ON c.parent=p.id WHERE c.id IN (1,4,5) ORDER BY c.id;
1) [[null,0],[null,null],[null,0]]
2) [[1,null],[4,2],[5,null]]
3) [[1,null],[4,2]]
4) [[2,0],[5,3],[6,0]]

24. 자체 모의 2회 24번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT COUNT(*) FROM t a CROSS JOIN t b WHERE a.id<=2 AND b.id<=3;
1) [[6]]
2) [[6],[6]]
3) [[7]]
4) [[null]]

25. 자체 모의 2회 25번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT p.id FROM t p WHERE EXISTS (SELECT 1 FROM t c WHERE c.parent=p.id) ORDER BY p.id;
1) [[1]]
2) [[2],[3]]
3) [[null],[null]]
4) [[1],[2]]

26. 자체 모의 2회 26번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id FROM t WHERE (grp,v) IN (SELECT grp,MAX(v) FROM t GROUP BY grp) ORDER BY id;
1) [[3],[4],[6]]
2) [[null],[null],[null]]
3) [[2],[3],[5]]
4) [[2],[3]]

27. 자체 모의 2회 27번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT v FROM t WHERE id<=3 UNION SELECT v FROM t WHERE id=5 ORDER BY v;
1) [[null],[null],[null]]
2) [[5],[15],[30]]
3) [[5],[15]]
4) [[6],[16],[31]]

28. 자체 모의 2회 28번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT COUNT(*) FROM (SELECT v FROM t WHERE id<=3 UNION ALL SELECT v FROM t WHERE id=5);
1) [[4]]
2) [[4],[4]]
3) [[5]]
4) [[null]]

29. 자체 모의 2회 29번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT v FROM t WHERE grp='A' INTERSECT SELECT v FROM t WHERE grp='B' ORDER BY v;
1) [[15],[15]]
2) [[16]]
3) [[null]]
4) [[15]]

30. 자체 모의 2회 30번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT v FROM t WHERE grp='A' EXCEPT SELECT v FROM t WHERE grp='B' ORDER BY v;
1) [[6]]
2) [[null]]
3) [[5]]
4) [[5],[5]]

31. 자체 모의 2회 31번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,RANK() OVER(ORDER BY v DESC),DENSE_RANK() OVER(ORDER BY v DESC) FROM t WHERE v IS NOT NULL ORDER BY id;
1) [[null,null,null],[null,null,null],[null,null,null],[null,null,null]]
2) [[1,4,3],[2,2,2],[3,2,2],[5,1,1]]
3) [[1,4,3],[2,2,2],[3,2,2]]
4) [[2,5,4],[3,3,3],[4,3,3],[6,2,2]]

32. 자체 모의 2회 32번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,SUM(COALESCE(v,0)) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t ORDER BY id;
1) [[1,5],[2,20],[3,35],[4,35],[5,65]]
2) [[1,5],[2,20],[3,35],[4,35]]
3) [[2,6],[3,21],[4,36],[5,36],[6,66]]
4) [[null,null],[null,null],[null,null],[null,null],[null,null]]

33. 자체 모의 2회 33번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,LAG(v) OVER(ORDER BY id),LEAD(v) OVER(ORDER BY id) FROM t ORDER BY id;
1) [[1,null,15],[2,5,15],[3,15,null],[4,15,30]]
2) [[2,0,16],[3,6,16],[4,16,0],[5,16,31],[6,0,0]]
3) [[null,0,null],[null,null,null],[null,null,0],[null,null,null],[null,0,0]]
4) [[1,null,15],[2,5,15],[3,15,null],[4,15,30],[5,null,null]]

34. 자체 모의 2회 34번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,LAST_VALUE(v) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t ORDER BY id;
1) [[2,31],[3,31],[4,31],[5,31],[6,31]]
2) [[null,null],[null,null],[null,null],[null,null],[null,null]]
3) [[1,30],[2,30],[3,30],[4,30],[5,30]]
4) [[1,30],[2,30],[3,30],[4,30]]

35. 자체 모의 2회 35번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,v FROM t WHERE v IS NOT NULL ORDER BY v DESC,id LIMIT 2;
1) [[null,null],[null,null]]
2) [[5,30],[2,15]]
3) [[5,30]]
4) [[6,31],[3,16]]

36. 자체 모의 2회 36번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
WITH RECURSIVE r(id,depth) AS (SELECT id,1 FROM t WHERE id=1 UNION ALL SELECT t.id,r.depth+1 FROM t JOIN r ON t.parent=r.id WHERE r.depth<5) SELECT * FROM r ORDER BY depth,id;
1) [[1,1],[2,2],[3,2],[4,3]]
2) [[1,1],[2,2],[3,2]]
3) [[2,2],[3,3],[4,3],[5,4]]
4) [[null,null],[null,null],[null,null],[null,null]]

37. 자체 모의 2회 37번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT SUM(CASE WHEN grp='A' THEN v END),SUM(CASE WHEN grp='B' THEN v END),SUM(CASE WHEN grp='C' THEN v END) FROM t;
1) [[20,15,30],[20,15,30]]
2) [[21,16,31]]
3) [[null,null,null]]
4) [[20,15,30]]

38. 자체 모의 2회 38번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT UPPER('ab'),SUBSTR('A-12',3,2),REPLACE('A-12','-',''),ROUND(2.346,2);
1) [["ab","","a12",3.35]]
2) [[null,null,null,null]]
3) [["AB","12","A12",2.35]]
4) [["AB","12","A12",2.35],["AB","12","A12",2.35]]

39. 자체 모의 2회 39번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT date('2024-02-28','+1 day'),date('2025-02-28','+1 day'),CAST('12' AS INTEGER)+1;
1) [[null,null,null]]
2) [["2024-02-29","2025-03-01",13]]
3) [["2024-02-29","2025-03-01",13],["2024-02-29","2025-03-01",13]]
4) [["","",14]]

40. 자체 모의 2회 40번. 아래 t는 이 문항의 독립 초기표다(열 id,grp,v,parent,flag; null은 SQL NULL).
[[1,"A",5,null,"ok"],[2,"A",15,1,"ok"],[3,"B",15,1,"no"],[4,"B",null,2,null],[5,"C",30,null,"ok"]]
SQLite에서 다음 질의의 행 배열은? 각 문항은 초기표에서 시작한다.
SELECT id,ROUND(1.0*v/SUM(v) OVER(PARTITION BY grp),3) FROM t WHERE grp='A' ORDER BY id;
1) [[1,0.25],[2,0.75]]
2) [[1,0.25]]
3) [[2,1.25],[3,1.75]]
4) [[null,null],[null,null]]

41. Oracle COALESCE(NULL,0,7)의 값은?
1) 7
2) NULL
3) 오류
4) 0

42. 정확히2행 대신 금액 2등까지 동점을 모두 뽑으려면?
1) ORDER BY 없이 LIMIT2
2) RANK에 고유id까지 넣어 동점 없애기
3) DENSE_RANK를 금액만으로 계산해<=2 필터
4) ROW_NUMBER<=2면 항상 동일

43. Oracle CUBE(지역,채널)가 ROLLUP(지역,채널)에 더하는 수준은?
1) 새 원천행 생성
2) 채널만의 소계
3) 총계 제거
4) 상세 제거

44. Oracle UNPIVOT 기본과 SQLite UNION ALL 세로변환의 NULL 차이는?
1) 기본 UNPIVOT은 NULL 셀 제외, UNION ALL은 보존
2) 둘 다 항상 제외
3) 둘 다 항상 오류
4) UNPIVOT은 원본 중복까지 모두 복구

45. CONNECT BY PRIOR parent_id=id는 어떤 방향인가?
1) 항상 아래로만
2) 정렬만 바꿈
3) 모든 행의 LEVEL이0
4) 자식에서 부모 방향으로 올라간다

46. ^A-[0-9]{2}$ 패턴에 개행/대소문자 조건을 별도로 명시하는 이유는?
1) 글자수가 자동 증가해서
2) Oracle과 Python의 모든 규칙이 동일해서
3) 엔진별 끝경계·플래그 차이로 과매칭을 막기 위해
4) 정규식은 항상 암호화라서

47. SQLite 자동커밋으로 UPDATE한 뒤 BEGIN 없이 ROLLBACK했다. 왜 복구되지 않나?
1) WHERE가 있으면 자동 저장점 생성
2) 갱신이 이미 확정되었고 활성 거래가 없다
3) ROLLBACK은 항상 DROP을 뜻함
4) SELECT가 없어서

48. Oracle에서 DML UPDATE 후 문법적으로 유효한 DDL이 실행 오류를 냈다. 직전 DML은 안전하게 롤백 가능한가?
1) DDL 직전 암묵 COMMIT 때문에 이미 확정될 수 있다
2) DDL실패면 무조건 전부취소
3) SQLite와 항상동일
4) DCL만 커밋한다

49. Oracle MERGE 원본에 같은 대상키가 여러 번 나온다. 먼저 확인할 것은?
1) 오류를 무시하고 재시도
2) 정렬만 바꾸면 유일해짐
3) 조건 없이 전체 DELETE
4) 원본 매칭키 유일성과 대상의 중복 갱신 위험

50. CREATE ROLE 권한과 SELECT ON 표 권한의 구분은?
1) 둘 다 비밀번호
2) 권한은 모델 프롬프트로만 강제
3) 전자는 시스템권한, 후자는 객체권한
4) 둘 다 행 데이터

문항별 정답·구체 해설

AX4U 독자 모의 2회 — 원기출 복원 아님 / 공식 합격 보장 없음
가상 데이터만 사용. 시험시간 90분은 학습자가 타이머로 잰다. 웹 자동 시간제한 기능을 뜻하지 않는다.
1~10: 모델링20점, 11~50: SQL80점, 문항당2점. 두 회차는 병렬형: 계산30문항의 수요값을 바꾸고 모델링·방언/관리20문항은 다른 질문을 사용한다.
총60점 이상이면서 모델링8점 이상, SQL32점 이상을 자체 진단기준으로 사용. 독립 SQL 작성과 오류복구는 기존 A~D 실기과제로 별도평가.
문제지와 seed만 먼저 열고 해설·exam-cases.json·코드는 제출 뒤 확인한다.

1. 정답 4
사본번호→ISBN→도서명의 이행종속을 나누고 FK로 연결한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

2. 정답 3
학생→이름은 전체 복합키 일부에 대한 종속이다. 학생 표로 분리해야 반복 갱신을 줄인다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

3. 정답 2
판매 전 상품도 존재한다. 자식0개 허용과 자식의 부모 필수참조가 동시에 성립한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

4. 정답 1
외부 식별자가 자식 주키의 일부다. 대응수는 별도이며 이 선언만으로1:1이 되지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

5. 정답 4
열의 업무 도메인을 원자적인 지점으로 정의한 뒤 반복 그룹을 분리한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

6. 정답 3
논리 구조 변경과 외부 조회계약을 분리하는 설계다. 호환성이 자동으로 생기는 것은 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

7. 정답 2
공통 I가 IC를 결정하므로 무손실이지만, 한 학생이 같은 과목의 두 강사를 갖는 모순은 JOIN을 보아야 검출할 수 있다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

8. 정답 1
최소성은 속성을 제거해도 유일성이 유지되는지로 판정한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

9. 정답 4
CHECK는 FALSE를 거부하지만 UNKNOWN은 허용될 수 있다. 필수값 제약이 별도로 필요하다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

10. 정답 3
반정규화는 성능 가설이다. 읽기/쓰기 부하, 갱신 주기, 정합성 복구를 측정하고 책임을 명시한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

11. 정답 2
COUNT(*)는 NULL행도 포함하고 COUNT(v)는 v가 있는 네 행만 센다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

12. 정답 1
비NULL 네 값의 합을4로 나눈다. NULL을0으로 채운 평균과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

13. 정답 4
NULL과 =로 비교한 결과는 UNKNOWN이다. WHERE는 TRUE만 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

14. 정답 3
IS NULL은 결측을 직접 검사한다. id4만 결측이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

15. 정답 2
BETWEEN은 양끝을 포함한다. 범위 밖과 NULL은 남지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

16. 정답 1
AND가 OR보다 먼저 묶인다. A는 모두, B는 ok만 통과하지만 B에는 ok가 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

17. 정답 4
1 이외의 값도 NULL과 다르다는 비교가 UNKNOWN이다. NULL을 제외하거나 NOT EXISTS로 고친다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

18. 정답 3
CASE는 처음 TRUE가 되는 분기를 택한다. NULL을 먼저 별도로 표시한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

19. 정답 2
NULLIF는20과 같을 때만 NULL로 바꾼다. 원본 NULL은 유지되고 COALESCE만 표시0을 만든다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

20. 정답 1
A 두 행, B 두 행, C 한 행이다. B의 합은 결측을 빼고 계산하지만 COUNT(*)는2다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

21. 정답 4
HAVING은 그룹합이 만들어진 뒤 필터한다. WHERE로 SUM 조건을 옮길 수 없다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

22. 정답 3
부모가 있는2·3·4만 연결된다. 자기조인은 같은 표를 역할별 별칭으로 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

23. 정답 2
부모 없는1·5도 LEFT라 남는다. 부모열만 NULL로 확장된다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

24. 정답 1
왼쪽2행과 오른쪽3행의 모든 짝은2×3=6개다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

25. 정답 4
자식이 둘인1도 한 행이다. EXISTS는 바깥 행마다 존재 여부만 판정한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

26. 정답 3
그룹별 최대와 (그룹,값) 쌍을 비교한다. NULL행은 최대값의 대표가 아니다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

27. 정답 2
v만 선택했으므로20 또는15의 중복이 제거된다. id까지 선택하면 다른 결과다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

28. 정답 1
UNION ALL은 중복까지 보존해3+1=4행이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

29. 정답 4
A와 B 모두에서 나타난 값만 하나씩 남긴다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

30. 정답 3
A에만 있는 값이다. Oracle19c의 같은 목적 구문은 MINUS다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

31. 정답 2
동점 다음 RANK는 건너뛰고 DENSE_RANK는 서로 다른 값의 순서다. NULL은 계산 전에 제외했다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

32. 정답 1
id 순서로 누적하며4의 NULL을0으로 표시했다. 이 표시정책이 실제 값0을 증명하지는 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

33. 정답 4
이전/다음 행으로 이동한다. 경계와 원자료 NULL 모두 NULL 결과를 만들므로 행 위치와 같이 읽는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

34. 정답 3
프레임을 끝까지 펼쳤으므로 모든 행의 LAST_VALUE가 id5의 값이다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

35. 정답 2
정확히2행이며 동점은id 오름차순으로 푼다. 동점 모두 포함하는 등수 조건과 다르다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

36. 정답 1
뿌리1→자식2·3→손자4 순서다. 이 입력은 순환이 없으며 깊이제한만으로 일반 트리 정제를 보장하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

37. 정답 4
조건별 합계를 가로로 펼친다. 중복 원행과 순서는 집계 과정에서 사라진다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

38. 정답 3
대문자화·1기반 세번째부터2글자·하이픈 제거·소수둘째자리 반올림을 각각 계산한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

39. 정답 2
2024는 윤년,2025는 평년이다. 숫자문자는 명시적으로 변환한다. SQLite 날짜함수를 Oracle에 그대로 복사하지 않는다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

40. 정답 1
같은 그룹 합을 분모로 쓴다. 정수 나눗셈을 피하려고1.0을 곱한다. 0합이면 NULLIF 등의 정책을 추가한다. 중간값을 구한 뒤 선택 행과 ORDER BY를 확인한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

41. 정답 4
첫 비NULL 값이0이다. NVL/NVL2와 입력개수·타입변환 규칙이 항상 같은 것은 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

42. 정답 3
서로 다른 상위2개 금액 수준을 의미한다. 정확히2행 정책과 분리하고 최종 출력에는 고유키 정렬을 더한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

43. 정답 2
두 열 CUBE는 상세·지역·채널·총계의 네 그룹집합이다. 각 수준의 합을 모두 더하면 중복계산이다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

44. 정답 1
INCLUDE NULLS 또는 SQLite WHERE IS NOT NULL로 정책을 맞춘다. 피벗 집계에서 사라진 원행은 복구되지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

45. 정답 4
PRIOR가 가리키는 이전 행의 parent_id와 현재 행의 id를 맞춘다. 시작행을 명시하고 순환도 점검한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

46. 정답 3
ASCII두자리·길이4·개행금지·대문자라는 계약을 같이 검사한다. Python UDF를 Oracle 전체호환이라 주장하지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

47. 정답 2
거래 경계 전에 확정된 변경은 롤백 대상이 아니다. 실습에선 메모리DB를 초기화하고 다시 명시거래로 시작한다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

48. 정답 1
Oracle은 문법적으로 유효한 DDL 직전에 커밋한다. 성공 직후 커밋도 별도다. DDL을 DML복구용 저장점 사이에 두지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

49. 정답 4
한 대상행을 여러 번 바꾸는 위험을 원본 집계/중복정책으로 해결한다. SQLite UPSERT 성공이 Oracle 성공 증거는 아니다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

50. 정답 3
역할은 권한 묶음이고 역할 생성 자체와 특정 객체 읽기는 다른 권한이다. 새 실권한은 이 실습에서 변경하지 않는다. 정답만 외우지 말고 조건을 바꾸어 결과를 설명한다.

모의시험 전용 전체 DDL·합성 seed

CREATE TABLE t(id INTEGER PRIMARY KEY,grp TEXT,v INTEGER,parent INTEGER,flag TEXT);
INSERT INTO t VALUES (1,'A',5,NULL,'ok'),(2,'A',15,1,'ok'),(3,'B',15,1,'no'),(4,'B',NULL,2,NULL),(5,'C',30,NULL,'ok');

30 계산문항 독립 고정 정답

[
  {
    "id": "sql-exam-two-11",
    "sql": "SELECT COUNT(*),COUNT(v) FROM t",
    "expected": [
      [
        5,
        4
      ]
    ],
    "reason": "COUNT(*)는 NULL행도 포함하고 COUNT(v)는 v가 있는 네 행만 센다."
  },
  {
    "id": "sql-exam-two-12",
    "sql": "SELECT SUM(v),AVG(v) FROM t",
    "expected": [
      [
        65,
        16.25
      ]
    ],
    "reason": "비NULL 네 값의 합을4로 나눈다. NULL을0으로 채운 평균과 다르다."
  },
  {
    "id": "sql-exam-two-13",
    "sql": "SELECT id FROM t WHERE v=NULL ORDER BY id",
    "expected": [],
    "reason": "NULL과 =로 비교한 결과는 UNKNOWN이다. WHERE는 TRUE만 남긴다."
  },
  {
    "id": "sql-exam-two-14",
    "sql": "SELECT id FROM t WHERE v IS NULL ORDER BY id",
    "expected": [
      [
        4
      ]
    ],
    "reason": "IS NULL은 결측을 직접 검사한다. id4만 결측이다."
  },
  {
    "id": "sql-exam-two-15",
    "sql": "SELECT id FROM t WHERE v BETWEEN 10 AND 20 ORDER BY id",
    "expected": [
      [
        2
      ],
      [
        3
      ]
    ],
    "reason": "BETWEEN은 양끝을 포함한다. 범위 밖과 NULL은 남지 않는다."
  },
  {
    "id": "sql-exam-two-16",
    "sql": "SELECT id FROM t WHERE grp='A' OR grp='B' AND flag='ok' ORDER BY id",
    "expected": [
      [
        1
      ],
      [
        2
      ]
    ],
    "reason": "AND가 OR보다 먼저 묶인다. A는 모두, B는 ok만 통과하지만 B에는 ok가 없다."
  },
  {
    "id": "sql-exam-two-17",
    "sql": "SELECT id FROM t WHERE id NOT IN (1,NULL) ORDER BY id",
    "expected": [],
    "reason": "1 이외의 값도 NULL과 다르다는 비교가 UNKNOWN이다. NULL을 제외하거나 NOT EXISTS로 고친다."
  },
  {
    "id": "sql-exam-two-18",
    "sql": "SELECT id,CASE WHEN v IS NULL THEN 'missing' WHEN v>=20 THEN 'high' ELSE 'low' END FROM t ORDER BY id",
    "expected": [
      [
        1,
        "low"
      ],
      [
        2,
        "low"
      ],
      [
        3,
        "low"
      ],
      [
        4,
        "missing"
      ],
      [
        5,
        "high"
      ]
    ],
    "reason": "CASE는 처음 TRUE가 되는 분기를 택한다. NULL을 먼저 별도로 표시한다."
  },
  {
    "id": "sql-exam-two-19",
    "sql": "SELECT id,COALESCE(v,0),NULLIF(v,20) FROM t WHERE id IN (3,4) ORDER BY id",
    "expected": [
      [
        3,
        15,
        15
      ],
      [
        4,
        0,
        null
      ]
    ],
    "reason": "NULLIF는20과 같을 때만 NULL로 바꾼다. 원본 NULL은 유지되고 COALESCE만 표시0을 만든다."
  },
  {
    "id": "sql-exam-two-20",
    "sql": "SELECT grp,COUNT(*),SUM(v) FROM t GROUP BY grp ORDER BY grp",
    "expected": [
      [
        "A",
        2,
        20
      ],
      [
        "B",
        2,
        15
      ],
      [
        "C",
        1,
        30
      ]
    ],
    "reason": "A 두 행, B 두 행, C 한 행이다. B의 합은 결측을 빼고 계산하지만 COUNT(*)는2다."
  },
  {
    "id": "sql-exam-two-21",
    "sql": "SELECT grp,SUM(v) FROM t GROUP BY grp HAVING SUM(v)>=20 ORDER BY grp",
    "expected": [
      [
        "A",
        20
      ],
      [
        "C",
        30
      ]
    ],
    "reason": "HAVING은 그룹합이 만들어진 뒤 필터한다. WHERE로 SUM 조건을 옮길 수 없다."
  },
  {
    "id": "sql-exam-two-22",
    "sql": "SELECT c.id,p.id FROM t c JOIN t p ON c.parent=p.id ORDER BY c.id",
    "expected": [
      [
        2,
        1
      ],
      [
        3,
        1
      ],
      [
        4,
        2
      ]
    ],
    "reason": "부모가 있는2·3·4만 연결된다. 자기조인은 같은 표를 역할별 별칭으로 읽는다."
  },
  {
    "id": "sql-exam-two-23",
    "sql": "SELECT c.id,p.id FROM t c LEFT JOIN t p ON c.parent=p.id WHERE c.id IN (1,4,5) ORDER BY c.id",
    "expected": [
      [
        1,
        null
      ],
      [
        4,
        2
      ],
      [
        5,
        null
      ]
    ],
    "reason": "부모 없는1·5도 LEFT라 남는다. 부모열만 NULL로 확장된다."
  },
  {
    "id": "sql-exam-two-24",
    "sql": "SELECT COUNT(*) FROM t a CROSS JOIN t b WHERE a.id<=2 AND b.id<=3",
    "expected": [
      [
        6
      ]
    ],
    "reason": "왼쪽2행과 오른쪽3행의 모든 짝은2×3=6개다."
  },
  {
    "id": "sql-exam-two-25",
    "sql": "SELECT p.id FROM t p WHERE EXISTS (SELECT 1 FROM t c WHERE c.parent=p.id) ORDER BY p.id",
    "expected": [
      [
        1
      ],
      [
        2
      ]
    ],
    "reason": "자식이 둘인1도 한 행이다. EXISTS는 바깥 행마다 존재 여부만 판정한다."
  },
  {
    "id": "sql-exam-two-26",
    "sql": "SELECT id FROM t WHERE (grp,v) IN (SELECT grp,MAX(v) FROM t GROUP BY grp) ORDER BY id",
    "expected": [
      [
        2
      ],
      [
        3
      ],
      [
        5
      ]
    ],
    "reason": "그룹별 최대와 (그룹,값) 쌍을 비교한다. NULL행은 최대값의 대표가 아니다."
  },
  {
    "id": "sql-exam-two-27",
    "sql": "SELECT v FROM t WHERE id<=3 UNION SELECT v FROM t WHERE id=5 ORDER BY v",
    "expected": [
      [
        5
      ],
      [
        15
      ],
      [
        30
      ]
    ],
    "reason": "v만 선택했으므로20 또는15의 중복이 제거된다. id까지 선택하면 다른 결과다."
  },
  {
    "id": "sql-exam-two-28",
    "sql": "SELECT COUNT(*) FROM (SELECT v FROM t WHERE id<=3 UNION ALL SELECT v FROM t WHERE id=5)",
    "expected": [
      [
        4
      ]
    ],
    "reason": "UNION ALL은 중복까지 보존해3+1=4행이다."
  },
  {
    "id": "sql-exam-two-29",
    "sql": "SELECT v FROM t WHERE grp='A' INTERSECT SELECT v FROM t WHERE grp='B' ORDER BY v",
    "expected": [
      [
        15
      ]
    ],
    "reason": "A와 B 모두에서 나타난 값만 하나씩 남긴다."
  },
  {
    "id": "sql-exam-two-30",
    "sql": "SELECT v FROM t WHERE grp='A' EXCEPT SELECT v FROM t WHERE grp='B' ORDER BY v",
    "expected": [
      [
        5
      ]
    ],
    "reason": "A에만 있는 값이다. Oracle19c의 같은 목적 구문은 MINUS다."
  },
  {
    "id": "sql-exam-two-31",
    "sql": "SELECT id,RANK() OVER(ORDER BY v DESC),DENSE_RANK() OVER(ORDER BY v DESC) FROM t WHERE v IS NOT NULL ORDER BY id",
    "expected": [
      [
        1,
        4,
        3
      ],
      [
        2,
        2,
        2
      ],
      [
        3,
        2,
        2
      ],
      [
        5,
        1,
        1
      ]
    ],
    "reason": "동점 다음 RANK는 건너뛰고 DENSE_RANK는 서로 다른 값의 순서다. NULL은 계산 전에 제외했다."
  },
  {
    "id": "sql-exam-two-32",
    "sql": "SELECT id,SUM(COALESCE(v,0)) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM t ORDER BY id",
    "expected": [
      [
        1,
        5
      ],
      [
        2,
        20
      ],
      [
        3,
        35
      ],
      [
        4,
        35
      ],
      [
        5,
        65
      ]
    ],
    "reason": "id 순서로 누적하며4의 NULL을0으로 표시했다. 이 표시정책이 실제 값0을 증명하지는 않는다."
  },
  {
    "id": "sql-exam-two-33",
    "sql": "SELECT id,LAG(v) OVER(ORDER BY id),LEAD(v) OVER(ORDER BY id) FROM t ORDER BY id",
    "expected": [
      [
        1,
        null,
        15
      ],
      [
        2,
        5,
        15
      ],
      [
        3,
        15,
        null
      ],
      [
        4,
        15,
        30
      ],
      [
        5,
        null,
        null
      ]
    ],
    "reason": "이전/다음 행으로 이동한다. 경계와 원자료 NULL 모두 NULL 결과를 만들므로 행 위치와 같이 읽는다."
  },
  {
    "id": "sql-exam-two-34",
    "sql": "SELECT id,LAST_VALUE(v) OVER(ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) FROM t ORDER BY id",
    "expected": [
      [
        1,
        30
      ],
      [
        2,
        30
      ],
      [
        3,
        30
      ],
      [
        4,
        30
      ],
      [
        5,
        30
      ]
    ],
    "reason": "프레임을 끝까지 펼쳤으므로 모든 행의 LAST_VALUE가 id5의 값이다."
  },
  {
    "id": "sql-exam-two-35",
    "sql": "SELECT id,v FROM t WHERE v IS NOT NULL ORDER BY v DESC,id LIMIT 2",
    "expected": [
      [
        5,
        30
      ],
      [
        2,
        15
      ]
    ],
    "reason": "정확히2행이며 동점은id 오름차순으로 푼다. 동점 모두 포함하는 등수 조건과 다르다."
  },
  {
    "id": "sql-exam-two-36",
    "sql": "WITH RECURSIVE r(id,depth) AS (SELECT id,1 FROM t WHERE id=1 UNION ALL SELECT t.id,r.depth+1 FROM t JOIN r ON t.parent=r.id WHERE r.depth<5) SELECT * FROM r ORDER BY depth,id",
    "expected": [
      [
        1,
        1
      ],
      [
        2,
        2
      ],
      [
        3,
        2
      ],
      [
        4,
        3
      ]
    ],
    "reason": "뿌리1→자식2·3→손자4 순서다. 이 입력은 순환이 없으며 깊이제한만으로 일반 트리 정제를 보장하지 않는다."
  },
  {
    "id": "sql-exam-two-37",
    "sql": "SELECT SUM(CASE WHEN grp='A' THEN v END),SUM(CASE WHEN grp='B' THEN v END),SUM(CASE WHEN grp='C' THEN v END) FROM t",
    "expected": [
      [
        20,
        15,
        30
      ]
    ],
    "reason": "조건별 합계를 가로로 펼친다. 중복 원행과 순서는 집계 과정에서 사라진다."
  },
  {
    "id": "sql-exam-two-38",
    "sql": "SELECT UPPER('ab'),SUBSTR('A-12',3,2),REPLACE('A-12','-',''),ROUND(2.346,2)",
    "expected": [
      [
        "AB",
        "12",
        "A12",
        2.35
      ]
    ],
    "reason": "대문자화·1기반 세번째부터2글자·하이픈 제거·소수둘째자리 반올림을 각각 계산한다."
  },
  {
    "id": "sql-exam-two-39",
    "sql": "SELECT date('2024-02-28','+1 day'),date('2025-02-28','+1 day'),CAST('12' AS INTEGER)+1",
    "expected": [
      [
        "2024-02-29",
        "2025-03-01",
        13
      ]
    ],
    "reason": "2024는 윤년,2025는 평년이다. 숫자문자는 명시적으로 변환한다. SQLite 날짜함수를 Oracle에 그대로 복사하지 않는다."
  },
  {
    "id": "sql-exam-two-40",
    "sql": "SELECT id,ROUND(1.0*v/SUM(v) OVER(PARTITION BY grp),3) FROM t WHERE grp='A' ORDER BY id",
    "expected": [
      [
        1,
        0.25
      ],
      [
        2,
        0.75
      ]
    ],
    "reason": "같은 그룹 합을 분모로 쓴다. 정수 나눗셈을 피하려고1.0을 곱한다. 0합이면 NULLIF 등의 정책을 추가한다."
  }
]

메모리 SQLite 실제 검산

import json,sqlite3
from pathlib import Path
P=Path(__file__).resolve().parent
cases=json.loads((P/'exam-cases.json').read_text(encoding='utf-8'))
for case in cases:
 db=sqlite3.connect(':memory:')
 db.executescript((P/'exam-seed.sqlite.sql').read_text(encoding='utf-8'))
 got=[list(x) for x in db.execute(case['sql'])]
 assert got==case['expected'],(case['id'],got,case['expected'])
 db.close()
print('PASS 30 SQL exam cases; SQLite only; Oracle NOT RUN')

검산 예상 stdout

PASS 30 SQL exam cases; SQLite only; Oracle NOT RUN

자체 답안 채점·경계 자가시험

import json,sys
from pathlib import Path
P=Path(__file__).resolve().parent
key=json.loads((P/'exam-key.json').read_text(encoding='utf-8'))
def score(a):
 if len(a)!=50 or any(type(v) is not int or v not in range(5) for v in a):
  raise ValueError('답안은 50개 정수: 미응답0 또는 선택지1~4')
 flags=[x==y for x,y in zip(a,key)]
 model=2*sum(flags[:10]);sql=2*sum(flags[10:]);total=model+sql
 return model,sql,total,total>=60 and model>=8 and sql>=32
if '--self-test' in sys.argv:
 assert score(key)==(20,80,100,True)
 assert score([0]*50)==(0,0,0,False)
 assert score([0]*10+key[10:])==(0,80,80,False)
 assert score(key[:10]+[0]*40)==(20,0,20,False)
 assert score(key[:4]+[0]*6+key[10:36]+[0]*14)==(8,52,60,True)
 try:score([True]*50)
 except ValueError:pass
 else:raise AssertionError('bool answers must be rejected')
 print('PASS exam grading boundaries; learning mastery NOT certified')
else:
 a=json.loads((P/'exam-response.json').read_text(encoding='utf-8'))
 m,s,t,ok=score(a)
 print(f'model={m}/20 sql={s}/80 total={t}/100 diagnostic_pass={ok}')
 print('독립 실기·오류복구·전이평가는 별도; 공식시험 합격/숙련 인증 아님')

채점 기준 선택지

[4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3, 2, 1, 4, 3]

학습자 답안: 미응답0을 선택지1~4로 변경

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

빈답안 채점 예상

model=0/20 sql=0/80 total=0/100 diagnostic_pass=False
독립 실기·오류복구·전이평가는 별도; 공식시험 합격/숙련 인증 아님
실습 해설 보기

A: 정확히2행은20/30 각각3석이며 동점 포함1등도20/30이다. B: 새 분류5의 부모는4, 음악 완료 좌석합은3이다. C: 합7→6,102는 취소로 복구한다. D: DELETE WHERE/일반 Oracle TRUNCATE/DROP/REVOKE SELECT를 구별한다. SQLite에는 TRUNCATE와 DCL 직접 구문이 없다. 105가 완료1석이면1등은30의3석 한 행이다. 일정 등은 공식 회차 공지 조회 전 미확인으로 남긴다. 50문항의 문항별 정답·해설은 이 단원의 exam-answers.txt에 모두 제공된다. 계산문항의 고정 정답은 exam-cases.json과 exam-verify.py로 실제 대조한다.

평가 기준: A의 동점 정책, B의 관계 방향, C의 저장점 복구, D의 방언·권한을 각각 결과·중간 근거·오류 원인·변형 검증으로 평가한다. 모든 항목 충족 시 소규모 진단 통과다. 부족한 유형만 복습하며 객관식 점수와 독립 숙련은 따로 기록한다. 모의시험은 문항당2점으로 별도 채점하고, 오답마다 중간값/규칙 오류와 입력변형 재시험 근거가 있어야 보완 완료다. 실기 미통과를 높은 객관식 점수로 대체하지 않는다.

새 맥락에 적용하기

공구 분류·대여·재고로 A~D를 옮겨 동점·NULL·순환·잘못된 갱신 반례를 작성하라. 각 반례의 수정 전후 실행값과 한계를 제출해야 통과한다. 독립 전이·실패주입: 새 가상 공구대여 자료: 물품 D1, D2; 요청 R1(D1,완료), R2(D1,취소), R3(D2,완료), R4(D2,완료). 물품별 완료 건수는 D1=1,D2=2다. SQLite에서 물품 LEFT JOIN 요청 후 ON에 완료 조건을 둔 집계와, 전체 조인에 WHERE 완료 조건을 두어 미대여 물품 D3가 사라지는 실패를 비교한다. 완료건 0인 D3를 추가해 결과 1/2/0을 예측 후 실행한다.

내 말로 설명하고 가르치기

학습자는 다음 핵심 문장을 쉬운 말로 풀어 설명하고, 이 단원 예제의 중간값 하나를 다시 계산한다. “정답을 외우지 말고 입력을 바꿔도 같은 원리로 풀 수 있는지 확인한다.” 강사는 이 단원의 질문과 변형 입력을 가리고 제시한다. 학습자가 결과가 달라지는 이유, 비유의 한계, 오류 복구 방법을 설명하는지 확인한다. 용어를 외워 말하거나 정답 출력만 복사하면 해당 실습을 다시 수행한다. 독립 설명·인수 판정: 판정: COUNT(요청.id)와 COUNT(*) 차이, 취소 제외, D3 보존을 실제 SQL 출력으로 대조한다. D2 요청 1건을 NULL 상태로 바꿔 D2=1인지 확인한다. 별도 SQLITE 메모리 DB만 사용; 기존 50문항 점수는 이 전이과제를 대체하지 않는다. 동료에게 테이블 생성·INSERT·SELECT와 DB 재시작 시 자료 소멸을 인계한다.

실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.

읽음과 숙련은 별개입니다.

참고 출처와 확인 범위

  • https://www.dataq.or.kr/www/sub/a_04.do — 공식과목·배점·50문항·90분·합격/과락을실제조회. 회차별일정/신분증/입실세칙은미확인.
  • https://docs.python.org/3/using/index.html — 환경 준비 안내의 공식 플랫폼별 설치/인터프리터 실행 문서 실제 조회. Windows Install Manager와 macOS .pkg 경로를 추가 확인했다. 이 제작 중 신규 설치는 하지 않았다.

← 전체 과정