대상의 애매모호함을 없애고 현상을 정확하게 기술하는 특징은 명확화이다.
모델링의 세 특징은 현실을 일정한 형식으로 표현하는 추상화, 제한된 표기법으로 쉽게 이해하게 하는 단순화, 그리고 명확화이다.
- 추상화는 현실 세계를 일정한 형식에 맞추어 표현하는 것이다.
- 단순화는 제한된 표기법과 언어로 쉽게 이해하도록 표현하는 것이다.
- 정규화는 이상 현상을 없애는 설계 기법으로 세 특징에 들지 않는다.
ERD 는 엔터티 도출·배치 → 관계 설정 → 관계명 기술 → 관계 참여도 기술 → 관계 필수여부 기술 순으로 작성한다.
관계명을 붙인 뒤에는 한 인스턴스가 상대 인스턴스 몇 개와 관계를 맺는지(1 또는 N)를 먼저 표시하고, 마지막에 반드시·선택 여부를 표시한다.
- 엔터티 배치는 관계를 설정하기 전에 하는 단계다.
- 관계 설정은 관계명 기술보다 앞선 단계다.
- 관계 필수여부 기술은 참여도 기술 다음의 마지막 단계다.
속성 이름은 명사형의 간결한 업무 용어로 지어야 하며 서술식 속성명(예: 고객이 예매한 좌석의 번호)은 쓰지 않는다.
속성명은 해당 업무에서 쓰는 용어를 따르고, 약어를 줄이며, 데이터 모델 전체에서 유일하게 지어야 혼동이 없다.
- 현업에서 실제로 쓰는 용어를 속성명으로 쓰는 것이 원칙이다.
- 약어는 의미 전달을 어렵게 하므로 가급적 제한한다.
- 같은 이름이 여러 엔터티에서 다른 뜻으로 쓰이지 않게 유일하게 짓는다.
영화 쪽에서 상영 끝에 원(O)과 까마귀발이 있으므로 영화 하나는 상영을 0개 이상 가지며, 상영이 없는 영화도 존재할 수 있다.
관계선 끝의 원은 선택 참여, 바(|)는 필수 참여, 까마귀발은 N 을 뜻하며 상대편 끝의 기호를 읽는다.
- 회원 쪽에서 본 예매 끝에 원이 있어 예매가 없는 회원도 있다.
- 예매 쪽에서 본 상영 끝은 까마귀발 없이 바만 있어 예매 1건은 상영 1개에만 속한다.
- 상영 쪽에서 본 예매 끝에 원이 있어 예매 없는 상영도 가능하다.
이메일은 회원 인스턴스를 유일하게 구분할 수 있지만 주식별자로 쓰이지 않는 속성이므로 보조식별자에 해당한다.
상영은 영화에서 식별 관계로 받은 영화코드와 스스로 만든 상영순번을 합친 복합식별자이며, 관계로 받아 온 속성은 외부식별자이다.
- 상영의 주식별자는 두 속성으로 이루어진 복합식별자다.
- 예매의 영화코드는 상영과의 관계로 받은 외부식별자다.
- 상영순번은 상영이 스스로 생성한 내부식별자다.
상영관번호가 주식별자의 일부인 상영번호에만 종속되는 부분 함수 종속이 있으므로 [상영좌석]은 제1정규형이지만 제2정규형은 아니다.
속성이 모두 원자값이라 1NF 는 만족하며, 상영관번호·상영관명을 상영 쪽으로 분리해야 2NF 가 되고 상영관명의 이행 함수 종속은 그다음 단계에서 제거한다.
- 모든 속성이 원자값이므로 1NF 는 만족한다.
- 부분 함수 종속이 남아 있어 2NF 를 만족하지 못한다.
- 2NF 도 아니므로 3NF 까지 만족할 수 없다.
자기참조(재귀) 관계로 만든 계층 모델은 대분류·중분류처럼 단계가 늘어나도 행만 추가하면 되므로 엔터티 구조를 바꾸지 않는다.
상위 장르명은 같은 테이블을 별칭으로 두 번 쓰는 셀프 조인으로 조회하며, 최상위 장르는 부모가 없어 상위장르코드가 NULL 이다.
- 최상위 장르는 상위가 없으므로 상위장르코드가 NULL 이어야 한다.
- 같은 테이블을 셀프 조인하면 되므로 다른 테이블이 없어도 된다.
- 상위장르코드 속성이 하나뿐이라 부모는 최대 하나다.
항상 함께 생성·삭제되는 예매와 예매좌석은 업무상 하나의 단위이므로 하나의 트랜잭션으로 묶어야 하며 좌석 삭제만 따로 떼면 안 된다.
별도 트랜잭션으로 나누면 예매만 지워지고 좌석이 남는 중간 상태가 생겨 원자성과 일관성이 깨진다.
- 함께 존재해야 하므로 양쪽 모두 필수 참여 관계로 표현한다.
- 취소할 때 예매와 좌석을 모두 지워야 데이터가 맞는다.
- 한쪽만 처리되면 전체를 롤백해 작업 전 상태로 되돌린다.
SELECT COUNT(*) FROM 관람평 WHERE 한줄평 IS NULL; 을 실행했다. Oracle 과 SQL Server 의 결과를 차례대로 나타낸 것은?[관람평]
| 관람평번호 | 입력한 한줄평 값 |
|---|---|
| 1 | '재밌음' |
| 2 | '' (빈 문자열) |
| 3 | NULL |
| 4 | ' ' (공백 1칸) |
Oracle 은 빈 문자열을 NULL 로 저장하므로 관람평번호 2·3 이 IS NULL 에 해당해 2이고, SQL Server 는 빈 문자열을 길이 0 문자열로 따로 저장해 3만 세어 1이다.
공백 1칸은 길이 1인 문자 값이므로 두 DBMS 모두 NULL 이 아니다.
- Oracle 에서 빈 문자열이 NULL 로 바뀌는 점을 놓치면 1로 본다.
- 공백 1칸까지 NULL 로 보면 Oracle 이 3이 되지만 공백은 값이다.
- SQL Server 는 빈 문자열을 NULL 로 바꾸지 않아 2가 아니다.
대체 여부 기준에서 업무 규칙에 의해 자연스럽게 만들어지는 식별자가 본질식별자이고, 원조식별자가 복잡해 인위적으로 만든 것이 인조식별자이다.
예를 들어 (회원번호, 상영번호, 좌석번호) 대신 시스템이 붙인 예매번호는 인조식별자이며 보통 단일 순번 속성으로 만든다.
- 인조식별자는 대개 한 속성짜리 일련번호여서 복합키일 필요가 없다.
- 본질식별자는 업무 값으로 정해지며 순번으로 부여하는 것은 인조식별자의 방식이다.
- 부모에게서 상속받는지는 내부·외부식별자 구분 기준이다.
유일성과 최소성을 갖춘 후보키 중 기본키로 뽑히지 않은 나머지를 대체키(Alternate Key)라 하므로 이메일은 대체키이다.
후보키는 회원번호와 이메일 둘이고 그중 회원번호가 기본키가 되었으며, 대체키에는 보통 UNIQUE 제약을 걸어 유일성을 지킨다.
- 외래키는 다른 테이블의 기본키를 참조하는 열이다.
- 복합키는 두 개 이상의 열을 묶은 키인데 이메일은 단일 열이다.
- 인조키는 업무와 무관하게 시스템이 만든 일련번호 같은 키이다.
[예매]
| 예매번호 | 인원 | 단가 | 할인 |
|---|---|---|---|
| 1 | 2 | 12000 | 1000 |
| 2 | 1 | 12000 | (null) |
| 3 | (null) | 9000 | 0 |
| 4 | 3 | 9000 | 3000 |
SELECT 예매번호,
인원 * 단가 AS 정가합,
인원 * 단가 - 할인 AS 결제액
FROM 예매
ORDER BY 예매번호;| 예매번호 | 정가합 | 결제액 |
|---|---|---|
| 1 | 24000 | 23000 |
| 2 | 12000 | (null) |
| 3 | (null) | (null) |
| 4 | 27000 | 24000 |
| 예매번호 | 정가합 | 결제액 |
|---|---|---|
| 1 | 24000 | 23000 |
| 2 | 12000 | 12000 |
| 3 | (null) | (null) |
| 4 | 27000 | 24000 |
| 예매번호 | 정가합 | 결제액 |
|---|---|---|
| 1 | 24000 | 23000 |
| 2 | 12000 | (null) |
| 3 | 0 | 0 |
| 4 | 27000 | 24000 |
| 예매번호 | 정가합 | 결제액 |
|---|---|---|
| 1 | 24000 | 22000 |
| 2 | 12000 | (null) |
| 3 | (null) | (null) |
| 4 | 27000 | 18000 |
산술식에 NULL 이 하나라도 있으면 결과는 NULL이므로 할인이 NULL 인 예매번호 2 는 결제액이, 인원이 NULL 인 3 은 정가합과 결제액이 모두 NULL 이다.
곱셈이 뺄셈보다 먼저 계산되어 1은 24000-1000=23000, 4는 27000-3000=24000 이다.
- NULL 을 0으로 보고 빼면 2의 결제액이 12000 이 되지만 실제는 NULL 이다.
- NULL 과의 곱도 0이 아니라 NULL 이므로 3을 0으로 보면 틀린다.
- 단가-할인을 먼저 계산하면 22000·18000 이 나오지만 우선순위와 다르다.
Oracle 은 열 별칭에는 AS 를 허용하지만 테이블 별칭 앞에는 AS 를 쓸 수 없어 FROM 상영 S 처럼 바로 붙여 쓴다.
또 SELECT 절의 열 별칭은 WHERE 절보다 늦게 정해지므로 WHERE 에서는 쓸 수 없고 ORDER BY 에서만 쓸 수 있다.
- WHERE 절은 SELECT 보다 먼저 처리되어 열 별칭을 참조하면 오류가 난다.
- 테이블에 별칭을 주면 열은 그 별칭으로 한정해야 하고 원래 테이블명을 쓰면 오류가 난다.
- SELECT * 는 테이블에 정의된 열 순서대로 모든 열을 돌려준다.
[상영]
| 상영번호 | 상영코드 |
|---|---|
| 1 | 강남-07-1930 |
| 2 | 신촌역-03-2100 |
| 3 | 홍대-12-1015 |
| 4 | 일산호수-05-0900 |
SELECT 상영번호, ( ㉠ ) AS 관번호 FROM 상영 ORDER BY 상영번호;
[원하는 결과]
| 상영번호 | 관번호 |
|---|---|
| 1 | 07 |
| 2 | 03 |
| 3 | 12 |
| 4 | 05 |
INSTR(상영코드, '-') 가 첫 하이픈 위치를 돌려주므로 그 다음 위치부터 2글자를 자르면 지점명 길이와 관계없이 07·03·12·05 가 나온다.
지점명이 2~4글자로 달라 관번호의 시작 위치가 행마다 다르므로 고정 위치가 아닌 구분자 위치를 기준으로 잘라야 한다.
- 길이를 생략한 SUBSTR 은 끝까지 잘라 07-1930 처럼 시각까지 나온다.
- 3번째부터 3글자는 지점명이 2글자일 때만 맞고 신촌역은 '역-0' 이 된다.
- 4번째부터 2글자도 지점명이 2글자인 행에서만 관번호가 나온다.
[회원]
| 회원번호 | 휴대폰 | 이메일 | 포인트 |
|---|---|---|---|
| 1 | 010-11 | kim@a.kr | 3500 |
| 2 | (null) | lee@a.kr | 1200 |
| 3 | (null) | (null) | (null) |
| 4 | 010-44 | (null) | 800 |
SELECT 회원번호,
COALESCE(휴대폰, 이메일, '미등록') AS 연락처,
CASE WHEN 포인트 >= 1000 THEN 'VIP'
WHEN 포인트 >= 3000 THEN 'VVIP'
ELSE '일반' END AS 등급
FROM 회원
ORDER BY 회원번호;| 회원번호 | 연락처 | 등급 |
|---|---|---|
| 1 | 010-11 | VIP |
| 2 | 미등록 | VIP |
| 3 | 미등록 | 일반 |
| 4 | 010-44 | 일반 |
| 회원번호 | 연락처 | 등급 |
|---|---|---|
| 1 | 010-11 | VVIP |
| 2 | lee@a.kr | VIP |
| 3 | 미등록 | 일반 |
| 4 | 010-44 | 일반 |
| 회원번호 | 연락처 | 등급 |
|---|---|---|
| 1 | 010-11 | VIP |
| 2 | lee@a.kr | VIP |
| 3 | 미등록 | 일반 |
| 4 | 010-44 | 일반 |
| 회원번호 | 연락처 | 등급 |
|---|---|---|
| 1 | 010-11 | VIP |
| 2 | lee@a.kr | VIP |
| 3 | 미등록 | (null) |
| 4 | 010-44 | 일반 |
COALESCE 는 왼쪽부터 처음 만난 NOT NULL 값을 돌려주므로 2는 이메일, 3은 '미등록'이 되고, 검색 CASE 는 처음 참인 WHEN 에서 멈춰 3500 포인트인 1도 VIP 가 된다.
포인트가 NULL 인 3은 두 비교가 모두 UNKNOWN 이라 ELSE 의 '일반'을 받는다.
- 더 높은 조건을 찾아 VVIP 를 주지 않으며, 앞선 WHEN 이 먼저 참이 된다.
- ELSE 가 있으므로 NULL 비교 행도 NULL 이 아니라 '일반'이다.
- 휴대폰이 NULL 이면 다음 인수인 이메일을 보므로 2는 '미등록'이 아니다.
[회원]
| 회원번호 | 가입일 |
|---|---|
| 1 | 2026-01-31 |
| 2 | 2026-02-28 |
| 3 | 2026-03-15 |
| 4 | 2026-04-30 |
| 5 | 2026-05-30 |
SELECT 회원번호,
TO_CHAR(ADD_MONTHS(가입일, 1), 'YYYY-MM-DD') AS 만료일
FROM 회원
ORDER BY 회원번호;| 회원번호 | 만료일 |
|---|---|
| 1 | 2026-03-02 |
| 2 | 2026-03-30 |
| 3 | 2026-04-14 |
| 4 | 2026-05-30 |
| 5 | 2026-06-29 |
| 회원번호 | 만료일 |
|---|---|
| 1 | 2026-02-28 |
| 2 | 2026-03-31 |
| 3 | 2026-04-15 |
| 4 | 2026-05-31 |
| 5 | 2026-06-30 |
| 회원번호 | 만료일 |
|---|---|
| 1 | 2026-02-28 |
| 2 | 2026-03-28 |
| 3 | 2026-04-15 |
| 4 | 2026-05-30 |
| 5 | 2026-06-30 |
| 회원번호 | 만료일 |
|---|---|
| 1 | 2026-03-03 |
| 2 | 2026-03-31 |
| 3 | 2026-04-15 |
| 4 | 2026-05-31 |
| 5 | 2026-06-30 |
ADD_MONTHS 는 입력일이 그 달의 말일이면 결과도 결과 월의 말일로 맞추므로 2월 28일은 3월 31일, 4월 30일은 5월 31일이 된다.
1월 31일처럼 결과 월에 같은 날이 없으면 그 달 말일인 2월 28일이 되고, 말일이 아닌 3월 15일·5월 30일은 날짜만 그대로 옮겨진다.
- 말일 규칙을 빼고 같은 날짜로만 옮기면 3월 28일·5월 30일이 나온다.
- 존재하지 않는 2월 31일을 넘겨 3월로 보내지 않고 2월 말일에서 멈춘다.
- 한 달 더하기는 30일 더하기가 아니므로 3월 2일 같은 값이 나오지 않는다.
[영화]
| 영화번호 | 제목 | 관람등급 |
|---|---|---|
| 1 | 파도 | 12 |
| 2 | 여름밤 | 19 |
| 3 | 유리새 | (null) |
| 4 | 서울역 | 15 |
| 5 | 모래성 | (null) |
| 6 | 달무리 | 전체 |
SELECT 영화번호, 제목
FROM 영화
WHERE NOT (관람등급 = '19')
OR 관람등급 = NULL
ORDER BY 영화번호;관람등급이 NULL 인 행은 관람등급 = '19' 가 UNKNOWN 이고 NOT 을 붙여도 UNKNOWN 이며, = NULL 비교 역시 항상 UNKNOWN 이라 제외된다.
결국 등급이 있고 19가 아닌 파도·서울역·달무리 3행만 남으며, NULL 행을 찾으려면 IS NULL 을 써야 한다.
- = NULL 이 NULL 행을 찾는다고 보면 5행이 되지만 결과는 없다.
- NULL 비교를 참으로 보아 모든 행이 나온다고 보면 6행이다.
- 문자 '전체'도 '19'와 다른 값이므로 빠지지 않아 2행이 아니다.
[조건]
㉠ WHERE NOT 장르 = '액션' AND 평점 >= 4 ㉡ WHERE 장르 <> '액션' AND 평점 >= 4 ㉢ WHERE 평점 >= 4 AND NOT 장르 = '액션' ㉣ WHERE NOT (장르 = '액션' AND 평점 >= 4)
논리 연산자는 NOT → AND → OR 순으로 결합하므로 괄호 없는 NOT 은 바로 뒤의 장르 = '액션' 하나만 부정한다.
그래서 세 조건은 모두 '액션이 아니면서 평점 4 이상'이지만, ㉣은 괄호 전체를 부정해 액션이 아니거나 평점 4 미만인 행을 고른다.
- ㉠은 NOT 이 장르 비교에만 걸려 액션 외 장르 중 평점 4 이상을 고른다.
- ㉡은 <> 로 같은 조건을 직접 쓴 것이다.
- ㉢은 AND 양쪽의 순서만 바꾼 것으로 결과가 같다.
SELECT ( 식 ) AS 평균 FROM 관람평; 의 식 자리에 넣었을 때 결과가 나머지 셋과 다른 것은?[관람평]
| 관람평번호 | 영화번호 | 평점 |
|---|---|---|
| 1 | 1 | 4 |
| 2 | 1 | (null) |
| 3 | 2 | 5 |
| 4 | 2 | 3 |
| 5 | 3 | (null) |
[식]
㉠ AVG(평점) ㉡ SUM(평점) / COUNT(평점) ㉢ AVG(평점 + 0) ㉣ AVG(COALESCE(평점, 0))
COALESCE(평점, 0) 은 NULL 을 0으로 바꿔 다섯 행 모두를 평균에 넣으므로 12/5=2.4 가 되어 나머지와 다르다.
집계 함수는 NULL 을 제외하고 계산하므로 AVG(평점)과 SUM/COUNT(평점)은 12/3=4 이고, 평점 + 0 도 NULL 행은 NULL 그대로라 4이다.
- ㉠의 AVG 는 NULL 두 행을 빼고 세 값의 평균 4를 낸다.
- ㉡의 COUNT(평점)도 NULL 을 세지 않아 12/3=4 이다.
- ㉢의 평점 + 0 은 NULL 이 그대로 NULL 이라 ㉠과 같은 4이다.
[상영]
| 상영번호 | 상영관 | 관객수 |
|---|---|---|
| 1 | IMAX | 120 |
| 2 | IMAX | (null) |
| 3 | 2D | 90 |
| 4 | IMAX | 200 |
| 5 | (null) | 150 |
| 6 | 2D | (null) |
SELECT COUNT(*) AS 회차,
COUNT(관객수) AS 집계,
SUM(관객수) AS 합계
FROM 상영
WHERE 상영관 = 'IMAX'
HAVING SUM(관객수) > 300;| 회차 | 집계 | 합계 |
|---|---|---|
| 3 | 2 | 320 |
| 회차 | 집계 | 합계 |
|---|---|---|
| 3 | 3 | 320 |
| 회차 | 집계 | 합계 |
|---|
| 회차 | 집계 | 합계 |
|---|---|---|
| 2 | 2 | 320 |
WHERE 로 IMAX 세 행(120·NULL·200)만 남고, GROUP BY 가 없으므로 이 세 행 전체가 하나의 그룹이 되어 HAVING 합계 320 > 300 이 참이라 1행이 나온다.
COUNT(*)는 NULL 행도 세어 3, COUNT(관객수)는 NULL 을 빼 2이며 SELECT 에는 집계 함수만 있어 그룹 키 없이도 실행된다.
- COUNT(관객수)는 NULL 을 세지 않으므로 3이 아니다.
- COUNT(*)는 관객수가 NULL 인 행도 포함하므로 2가 아니다.
- HAVING 은 GROUP BY 없이도 전체를 한 그룹으로 보고 평가하므로 0행이 아니다.
[상영]
| 상영번호 | 영화번호 | 관객수 |
|---|---|---|
| 1 | M1 | 80 |
| 2 | M1 | 40 |
| 3 | M1 | 60 |
| 4 | M2 | 30 |
| 5 | M2 | 90 |
| 6 | M3 | (null) |
| 7 | M3 | 70 |
| 8 | M3 | 55 |
| 9 | M4 | 100 |
SELECT 영화번호, COUNT(*) AS 회차, SUM(관객수) AS 관객 FROM 상영 WHERE 관객수 >= 50 GROUP BY 영화번호 HAVING COUNT(*) >= 2 ORDER BY 영화번호;
| 영화번호 | 회차 | 관객 |
|---|---|---|
| M1 | 2 | 140 |
| M3 | 3 | 125 |
| 영화번호 | 회차 | 관객 |
|---|---|---|
| M1 | 2 | 140 |
| M2 | 1 | 90 |
| M3 | 2 | 125 |
| 영화번호 | 회차 | 관객 |
|---|---|---|
| M1 | 3 | 180 |
| M2 | 2 | 120 |
| M3 | 3 | 125 |
| 영화번호 | 회차 | 관객 |
|---|---|---|
| M1 | 2 | 140 |
| M3 | 2 | 125 |
WHERE 가 그룹화보다 먼저 실행되어 관객수 50 미만과 NULL 행(2·4·6)이 빠진 뒤 HAVING 이 남은 행 수를 센다.
그 결과 M1(80·60)과 M3(70·55)만 2회가 되어 남고, M2·M4 는 한 행만 남아 HAVING 에서 탈락한다.
- M2 는 WHERE 뒤 90 한 행만 남아 COUNT 가 1이므로 결과에 없다.
- NULL 은 50 이상 비교가 UNKNOWN 이라 M3 의 회차는 3이 아니다.
- WHERE 를 무시하고 모든 행을 센 결과는 실행 순서와 맞지 않는다.
[영화]
| 영화번호 | 제목 | 상영시간 | 개봉일 |
|---|---|---|---|
| 1 | 파도 | 128 | 2026-03-04 |
| 2 | 서울역 | 115 | 2026-03-11 |
| 3 | 모래성 | 102 | 2026-03-11 |
| 4 | 여름밤 | 95 | 2026-02-25 |
| 5 | 유리새 | 115 | 2026-03-04 |
SELECT 제목, 상영시간 AS 러닝 FROM 영화 ORDER BY 개봉일 DESC, 2;
| 제목 | 러닝 |
|---|---|
| 모래성 | 102 |
| 서울역 | 115 |
| 유리새 | 115 |
| 파도 | 128 |
| 여름밤 | 95 |
| 제목 | 러닝 |
|---|---|
| 여름밤 | 95 |
| 유리새 | 115 |
| 파도 | 128 |
| 모래성 | 102 |
| 서울역 | 115 |
| 제목 | 러닝 |
|---|---|
| 서울역 | 115 |
| 모래성 | 102 |
| 파도 | 128 |
| 유리새 | 115 |
| 여름밤 | 95 |
| 제목 | 러닝 |
|---|---|
| 여름밤 | 95 |
| 모래성 | 102 |
| 서울역 | 115 |
| 유리새 | 115 |
| 파도 | 128 |
개봉일은 SELECT 목록에 없어도 정렬 키로 쓸 수 있으며, 최신 개봉일(3월 11일)부터 내림차순으로 놓인다.
DESC 는 개봉일에만 적용되고 열 번호 2인 러닝은 기본 ASC 라 같은 날 개봉한 모래성(102)·서울역(115), 유리새(115)·파도(128) 순이 된다.
- 러닝까지 내림차순으로 보면 서울역이 모래성보다 앞서지만 둘째 키는 ASC 이다.
- 개봉일을 오름차순으로 보면 여름밤이 먼저 오지만 DESC 가 지정되어 있다.
- 러닝만으로 정렬한 결과로, 첫째 키인 개봉일을 무시했다.
[상영]
| 상영번호 | 시작시 |
|---|---|
| 1 | 9 |
| 2 | 10 |
| 3 | 15 |
| 4 | 22 |
| 5 | 23 |
| 6 | (null) |
[요금구간]
| 구간명 | 시작 | 끝 |
|---|---|---|
| 조조 | 7 | 10 |
| 일반 | 10 | 22 |
| 심야 | 23 | 27 |
SELECT S.상영번호, G.구간명 FROM 상영 S, 요금구간 G WHERE S.시작시 >= G.시작 AND S.시작시 < G.끝 ORDER BY S.상영번호;
| 상영번호 | 구간명 |
|---|---|
| 1 | 조조 |
| 3 | 일반 |
| 5 | 심야 |
| 상영번호 | 구간명 |
|---|---|
| 1 | 조조 |
| 2 | 조조 |
| 2 | 일반 |
| 3 | 일반 |
| 5 | 심야 |
| 상영번호 | 구간명 |
|---|---|
| 1 | 조조 |
| 2 | 일반 |
| 3 | 일반 |
| 5 | 심야 |
| 상영번호 | 구간명 |
|---|---|
| 1 | 조조 |
| 2 | 일반 |
| 3 | 일반 |
| 4 | 일반 |
| 5 | 심야 |
조건이 시작 이상·끝 미만이라 10시는 조조(끝 10)가 아닌 일반에만, 22시는 일반(끝 22)에도 심야(시작 23)에도 들지 않아 빠진다.
시작시가 NULL 인 상영번호 6은 비교가 UNKNOWN 이라 NON-EQUI 조인 결과에 없고, 최종 4행이 남는다.
- 끝값까지 포함해야 10시가 조조와 일반 두 곳에 매칭되지만 < 는 끝을 뺀다.
- 10시는 일반의 시작값과 같아 >= 조건을 만족하므로 빠지지 않는다.
- 22시는 일반의 끝값과 같아 < 조건에서 탈락한다.
SELECT * FROM 영화 M, 상영관 H, 시간대 T WHERE M.관람등급 = '전체';
세 테이블 사이에 조인 조건이 하나도 없으므로 카티션 곱이 만들어지고, WHERE 는 영화 쪽 행만 2행으로 줄인다.
따라서 결과는 2 × 3 × 4 = 24행이며 조건이 없는 상영관·시간대는 모든 행이 곱해진다.
- 필터를 무시하고 5×3×4 로 곱하면 60행이 된다.
- 행 수를 더한 2+3+4 는 조인 결과와 무관하다.
- 영화 조건만 남고 영화 행을 곱에서 빼면 12행이 되지만 2행이 곱해진다.
[영화]
| 영화번호 | 제목 |
|---|---|
| 1 | 파도 |
| 2 | 여름밤 |
| 3 | 유리새 |
[상영]
| 상영번호 | 영화번호 | 상영관 |
|---|---|---|
| 11 | 1 | 1관 |
| 12 | 1 | 3관 |
| 13 | 2 | 2관 |
| 14 | 9 | 1관 |
SELECT M.제목, S.상영관
FROM 상영 S ( ㉠ ) 영화 M
ON S.영화번호 = M.영화번호
ORDER BY M.영화번호, S.상영번호;[원하는 결과]
| 제목 | 상영관 |
|---|---|
| 파도 | 1관 |
| 파도 | 3관 |
| 여름밤 | 2관 |
| 유리새 | (null) |
원하는 결과에는 상영이 없는 유리새가 상영관 NULL 로 남고 영화번호 9 인 상영은 없으므로, 오른쪽에 쓴 영화를 모두 보존하는 RIGHT OUTER JOIN 이다.
FROM 에 상영을 먼저 썼으므로 상영이 왼쪽, 영화가 오른쪽이며 보존되는 쪽은 조인 키워드의 방향으로 정해진다.
- INNER JOIN 은 상영이 없는 유리새를 결과에서 뺀다.
- LEFT OUTER JOIN 은 상영 쪽을 보존해 영화가 없는 상영 14 가 제목 NULL 로 나온다.
- FULL OUTER JOIN 은 유리새와 상영 14 를 모두 남겨 한 행이 더 생긴다.
[영화]
| 영화번호 | 제목 | 장르 |
|---|---|---|
| 1 | 파도 | 액션 |
| 2 | 정원 | 드라마 |
| 3 | 추적 | 액션 |
| 4 | 별빛 | 애니 |
[상영]
| 상영번호 | 영화번호 | 관번호 |
|---|---|---|
| 101 | 1 | 1 |
| 102 | 1 | 2 |
| 103 | 2 | 1 |
| 104 | 4 | 3 |
SELECT M.제목, S.상영번호
FROM 영화 M
LEFT OUTER JOIN 상영 S
ON M.영화번호 = S.영화번호
AND M.장르 = '액션'
ORDER BY M.영화번호, S.상영번호;| 제목 | 상영번호 |
|---|---|
| 파도 | 101 |
| 파도 | 102 |
| 정원 | (null) |
| 추적 | (null) |
| 별빛 | (null) |
| 제목 | 상영번호 |
|---|---|
| 파도 | 101 |
| 파도 | 102 |
| 추적 | (null) |
| 제목 | 상영번호 |
|---|---|
| 파도 | 101 |
| 파도 | 102 |
| 정원 | 103 |
| 추적 | (null) |
| 별빛 | 104 |
| 제목 | 상영번호 |
|---|---|
| 파도 | 101 |
| 파도 | 102 |
LEFT OUTER JOIN 의 ON 절 조건은 결합 여부만 정하므로, 왼쪽 테이블에 건 M.장르 = '액션' 도 영화 행을 거르지 않고 매칭만 막는다.
액션인 파도는 101·102 와 결합하고, 상영이 없는 추적과 액션이 아닌 정원·별빛은 상영번호가 NULL 인 한 행씩 남아 5행이 된다.
- 3행 표는 장르 조건을 WHERE 절에 둔 결과로, 정원·별빛이 빠진다.
- 정원 103·별빛 104 가 붙은 표는 장르 조건을 무시한 결과이다.
- 2행 표는 INNER JOIN 처럼 결합된 행만 남긴 결과이다.
[상영]
| 상영번호 | 관번호 | 시작시각 |
|---|---|---|
| 201 | 1 | 10:00 |
| 202 | 2 | 13:00 |
| 203 | (null) | 15:00 |
| 204 | 1 | 18:00 |
[상영관]
| 관번호 | 관이름 |
|---|---|
| 1 | 1관 |
| 2 | 2관 |
| 3 | IMAX |
SELECT * FROM 상영 JOIN 상영관 USING (관번호) ORDER BY 상영번호;
| 상영번호 | 관번호 | 시작시각 | 관이름 |
|---|---|---|---|
| 201 | 1 | 10:00 | 1관 |
| 202 | 2 | 13:00 | 2관 |
| 204 | 1 | 18:00 | 1관 |
| 관번호 | 상영번호 | 시작시각 | 관이름 |
|---|---|---|---|
| 1 | 201 | 10:00 | 1관 |
| 2 | 202 | 13:00 | 2관 |
| (null) | 203 | 15:00 | (null) |
| 1 | 204 | 18:00 | 1관 |
| 상영번호 | 관번호 | 시작시각 | 관번호 | 관이름 |
|---|---|---|---|---|
| 201 | 1 | 10:00 | 1 | 1관 |
| 202 | 2 | 13:00 | 2 | 2관 |
| 204 | 1 | 18:00 | 1 | 1관 |
| 관번호 | 상영번호 | 시작시각 | 관이름 |
|---|---|---|---|
| 1 | 201 | 10:00 | 1관 |
| 2 | 202 | 13:00 | 2관 |
| 1 | 204 | 18:00 | 1관 |
USING 으로 조인하면 공통 열 관번호가 결과에 한 번만, 맨 앞에 나오고 나머지 열은 상영·상영관 순으로 이어진다.
INNER 조인이므로 관번호가 NULL 인 203 은 짝이 없어 빠지고, 상영이 없는 IMAX 도 나오지 않아 3행이다.
- 관번호가 상영 테이블 순서대로 두 번째에 오는 표는 공통 열의 위치를 잘못 본 것이다.
- 203 이 NULL 로 남은 표는 OUTER JOIN 결과이다.
- 관번호가 두 번 나오는 표는 ON 절 조인의 SELECT * 결과이다.
SELECT 예매번호, 금액
FROM 예매
WHERE 금액 연산자 (SELECT 금액
FROM 예매
WHERE 회원번호 = 7);< ANY 는 서브쿼리 결과 중 어느 하나보다만 작으면 참이므로, 실제로는 결과의 최댓값보다 작은 행을 모두 찾는다.
최솟값 미만인 행만 남기려면 전체 값과 비교하는 < ALL 을 써야 한다.
- = ANY 는 목록 중 하나와 같으면 참이라 IN 과 같은 결과를 낸다.
- > ALL 은 모든 값보다 커야 하므로 최댓값보다 큰 행만 남는다.
- 단일행 연산자 = 에 여러 행이 반환되면 실행 오류가 발생한다.
[예매]
| 예매번호 | 상영번호 | 인원 |
|---|---|---|
| 1 | 101 | 2 |
| 2 | 101 | 4 |
| 3 | 101 | 4 |
| 4 | 102 | 3 |
| 5 | 102 | (null) |
| 6 | 103 | 1 |
| 7 | 103 | 2 |
SELECT A.예매번호, A.상영번호, A.인원
FROM 예매 A
WHERE A.인원 = (SELECT MAX(B.인원)
FROM 예매 B
WHERE B.상영번호 = A.상영번호)
ORDER BY A.예매번호;| 예매번호 | 상영번호 | 인원 |
|---|---|---|
| 2 | 101 | 4 |
| 4 | 102 | 3 |
| 7 | 103 | 2 |
| 예매번호 | 상영번호 | 인원 |
|---|---|---|
| 2 | 101 | 4 |
| 3 | 101 | 4 |
| 4 | 102 | 3 |
| 5 | 102 | (null) |
| 7 | 103 | 2 |
| 예매번호 | 상영번호 | 인원 |
|---|---|---|
| 2 | 101 | 4 |
| 3 | 101 | 4 |
| 예매번호 | 상영번호 | 인원 |
|---|---|---|
| 2 | 101 | 4 |
| 3 | 101 | 4 |
| 4 | 102 | 3 |
| 7 | 103 | 2 |
연관 서브쿼리는 바깥 행의 상영번호마다 MAX(인원) 을 다시 구하므로, 상영별 최댓값과 같은 행이 동점까지 모두 남아 4행이다.
상영 101 은 인원 4 인 예매 2·3, 102 는 3 인 예매 4, 103 은 2 인 예매 7 이 남고, 인원이 NULL 인 예매 5 는 비교 결과가 UNKNOWN 이라 빠진다.
- 예매 2·3 만 남은 표는 상관 조건 없이 전체 최댓값과 비교한 결과이다.
- 상영별 한 행만 남긴 표는 동점 행을 하나로 줄인 것이다.
- 예매 5 가 포함된 표는 NULL 과의 비교를 참으로 본 것이다.
[영화]
| 영화번호 | 제목 |
|---|---|
| 1 | 파도 |
| 2 | 정원 |
| 3 | 추적 |
[예매]
| 예매번호 | 영화번호 |
|---|---|
| 11 | 1 |
| 12 | 1 |
| 13 | 3 |
| 14 | 1 |
| 15 | (null) |
SELECT M.제목,
(SELECT COUNT(*)
FROM 예매 R
WHERE ( ㉠ )) AS 건수
FROM 영화 M
ORDER BY M.영화번호;| 제목 | 건수 |
|---|---|
| 파도 | 3 |
| 정원 | 0 |
| 추적 | 1 |
바깥 영화 행마다 같은 영화의 예매만 세려면 스칼라 서브쿼리의 조건이 바깥 별칭 M 을 가리키는 상관 조건 R.영화번호 = M.영화번호 여야 한다.
예매가 없는 정원은 COUNT(*) 가 0 을 돌려 0 으로 나오고, 영화번호가 NULL 인 15번은 어느 영화에도 세지지 않는다.
- 한정자 없는 영화번호는 가장 가까운 R 의 열로 해석되어 NULL 이 아닌 예매 4건이 모든 영화에 세어진다.
- 1 = 1 은 상관 조건이 없어 모든 영화에 예매 5건이 그대로 세어진다.
- IN (1, 3) 은 파도·추적에 5, 정원에 0 을 주어 예매 건수와 다르다.
[온라인예매]
| 회원번호 | 금액 |
|---|---|
| 7 | 12000 |
| 3 | 9000 |
[현장예매]
| 회원번호 | 금액 |
|---|---|
| 5 | 12000 |
| 3 | 9000 |
SELECT 회원번호 AS 회원, 금액 FROM 온라인예매 UNION ALL SELECT 회원번호, 금액 FROM 현장예매 ORDER BY 금액 DESC, 회원;
| 회원번호 | 금액 |
|---|---|
| 5 | 12000 |
| 7 | 12000 |
| 3 | 9000 |
| 3 | 9000 |
| 회원 | 금액 |
|---|---|
| 5 | 12000 |
| 7 | 12000 |
| 3 | 9000 |
| 3 | 9000 |
| 회원 | 금액 |
|---|---|
| 7 | 12000 |
| 3 | 9000 |
| 5 | 12000 |
| 3 | 9000 |
| 회원 | 금액 |
|---|---|
| 5 | 12000 |
| 7 | 12000 |
| 3 | 9000 |
집합 연산의 ORDER BY 는 맨 끝에 한 번만 쓰여 합쳐진 전체 결과를 정렬하고, 결과 열 이름과 정렬 키는 첫 SELECT 의 별칭 회원을 따른다.
UNION ALL 은 중복을 지우지 않아 (3, 9000) 두 행이 모두 남고, 금액 내림차순 뒤 회원 오름차순으로 5·7·3·3 순이 된다.
- 온라인 행 뒤에 현장 행이 붙은 표는 정렬이 각 SELECT 에만 적용된다고 본 것이다.
- 3행 표는 UNION 처럼 중복을 제거한 결과이다.
- 열 이름이 회원번호인 표는 첫 SELECT 의 별칭을 무시한 것이다.
UNION ALL 은 중복 제거 단계를 거치지 않으므로, 중복이 없다고 보장되면 UNION 과 결과는 같으면서 정렬·비교 부담이 없다.
UNION 은 중복을 찾기 위해 정렬이나 해시 작업을 하지만, 그 결과의 순서를 보장하지는 않는다.
- 중복이 없으면 두 연산자의 결과 행 수는 같다.
- 순서가 필요하면 맨 끝에 ORDER BY 를 따로 써야 한다.
- UNION 은 NULL 을 같은 값으로 보아 합칠 뿐 NULL 행을 빼지 않는다.
[상영실적]
| 관이름 | 장르 | 관객수 |
|---|---|---|
| 1관 | 액션 | 120 |
| 1관 | 드라마 | 80 |
| 1관 | 액션 | 40 |
| 2관 | 액션 | 90 |
| 2관 | 애니 | 60 |
SELECT 관이름, 장르, SUM(관객수) AS 관객 FROM 상영실적 GROUP BY ( ㉠ ) ORDER BY 관이름, 장르;
| 관이름 | 장르 | 관객 |
|---|---|---|
| 1관 | 드라마 | 80 |
| 1관 | 액션 | 160 |
| 1관 | (null) | 240 |
| 2관 | 애니 | 60 |
| 2관 | 액션 | 90 |
| 2관 | (null) | 150 |
GROUP BY 관이름, ROLLUP(장르) 는 관이름을 늘 묶은 채 장르만 롤업하는 부분 ROLLUP 이라, 상세 행과 관별 소계만 만들고 총계는 만들지 않는다.
결과는 1관 드라마·액션과 소계 240, 2관 애니·액션과 소계 150 의 6행이며 소계 행의 장르가 NULL 이다.
- ROLLUP(관이름, 장르) 는 마지막에 전체 총계 390 행이 더 붙는다.
- GROUPING SETS(관이름, 장르) 는 관별·장르별 소계만 만들고 상세 행이 없다.
- CUBE 는 장르별 소계와 총계까지 모두 만든다.
[예매집계]
| 요일 | 관종류 | 인원 |
|---|---|---|
| 토 | 일반 | 3 |
| 토 | IMAX | 2 |
| 일 | 일반 | 4 |
| 일 | (null) | 1 |
| 토 | 일반 | 2 |
SELECT 요일, 관종류, SUM(인원) AS 인원 FROM 예매집계 GROUP BY CUBE(요일, 관종류) ORDER BY 요일, 관종류;
CUBE(요일, 관종류) 는 데이터에 실제로 있는 조합별 행 4개, 요일별 소계 2개, 관종류별 소계 3개, 총계 1개를 만들어 10행이 된다.
조합은 (토,일반)·(토,IMAX)·(일,일반)·(일,NULL) 이고, 원래 NULL 인 관종류도 하나의 값이라 관종류별 소계가 일반·IMAX·NULL 3개다.
- 7행은 관종류별 소계가 없는 ROLLUP 의 행 수이다.
- 9행은 원래 NULL 인 관종류의 소계를 빼고 센 값이다.
- 12행은 없는 조합까지 모두 만든다고 본 값이다.
[상영실적]
| 관이름 | 장르 | 관객수 |
|---|---|---|
| 1관 | 액션 | 50 |
| 1관 | (null) | 30 |
| 2관 | 액션 | 40 |
| 2관 | 액션 | 20 |
SELECT 관이름, 장르, SUM(관객수) AS 관객 FROM 상영실적 GROUP BY GROUPING SETS ((관이름, 장르), (관이름), ()) HAVING GROUPING(장르) = 1 ORDER BY 관이름, 장르;
GROUPING(장르) 는 장르가 집계로 인해 NULL 이 된 행에서만 1 이므로, HAVING 조건은 1관·2관 소계와 총계 3행만 남긴다.
GROUPING SETS 가 만든 행은 상세 3, 관별 소계 2, 총계 1 의 6행이며, 1관의 장르 NULL 상세 행은 원래 데이터의 NULL 이라 GROUPING 값이 0 이다.
- 2행은 총계 행의 GROUPING(장르) 도 1 이라는 점을 놓친 값이다.
- 4행은 원래 NULL 인 장르 상세 행까지 포함한 값이다.
- 6행은 HAVING 조건을 적용하기 전의 전체 행 수이다.
[상영]
| 상영번호 | 관이름 | 관객수 |
|---|---|---|
| 1 | 1관 | 30 |
| 2 | 1관 | 50 |
| 3 | 1관 | 20 |
| 4 | 2관 | 40 |
| 5 | 2관 | (null) |
| 6 | 2관 | 60 |
SELECT 상영번호,
RATIO_TO_REPORT(관객수)
OVER (PARTITION BY 관이름) AS 비율
FROM 상영
ORDER BY 상영번호;| 상영번호 | 비율 |
|---|---|
| 1 | 0.3 |
| 2 | 0.8 |
| 3 | 1 |
| 4 | 0.4 |
| 5 | (null) |
| 6 | 1 |
| 상영번호 | 비율 |
|---|---|
| 1 | 0.3 |
| 2 | 0.5 |
| 3 | 0.2 |
| 4 | 0.4 |
| 5 | (null) |
| 6 | 0.6 |
| 상영번호 | 비율 |
|---|---|
| 1 | 0.3 |
| 2 | 0.5 |
| 3 | 0.2 |
| 4 | 0.4 |
| 5 | 0 |
| 6 | 0.6 |
| 상영번호 | 비율 |
|---|---|
| 1 | 0.15 |
| 2 | 0.25 |
| 3 | 0.1 |
| 4 | 0.2 |
| 5 | (null) |
| 6 | 0.3 |
RATIO_TO_REPORT 는 각 행 값을 파티션 합계로 나눈 비율로, 1관은 합 100 기준 0.3·0.5·0.2, 2관은 NULL 을 뺀 합 100 기준 0.4·0.6 이다.
관객수가 NULL 인 상영 5 는 나눌 값이 없어 비율도 NULL 이며, 합계 계산에서도 제외된다.
- 0.15 부터 시작하는 표는 PARTITION BY 없이 전체 합 200 으로 나눈 결과이다.
- 상영 5 가 0 인 표는 NULL 을 0 으로 바꿔 계산한 것이다.
- 0.8·1 이 나오는 표는 누적 비율로 오해한 결과이다.
[상영]
| 상영번호 | 관이름 | 관객수 |
|---|---|---|
| 1 | A관 | 40 |
| 2 | A관 | 20 |
| 3 | B관 | 35 |
| 4 | B관 | (null) |
| 5 | C관 | 50 |
| 6 | A관 | 30 |
| 7 | B관 | 60 |
㉠
SELECT 관이름,
COUNT(*) OVER (PARTITION BY 관이름) AS 횟수
FROM 상영
WHERE 관객수 >= 30;㉡
SELECT DISTINCT 관이름,
COUNT(*) OVER (PARTITION BY 관이름) AS 횟수
FROM 상영
WHERE 관객수 >= 30;WHERE 가 먼저 관객수 30 이상인 1·3·5·6·7번 5행을 남기고, 윈도우 함수는 행을 줄이지 않으므로 ㉠은 5행이다.
㉡은 같은 관이름의 행이 같은 횟수 값을 가져 DISTINCT 로 A관 2·B관 2·C관 1 의 3행으로 줄어든다.
- 7은 WHERE 이전의 전체 행 수로, NULL 과 20 인 행도 빠진다.
- ㉡을 5행으로 본 것은 DISTINCT 가 중복 행을 합친다는 점을 놓친 것이다.
- ㉠을 3행으로 본 것은 윈도우 함수를 GROUP BY 처럼 그룹당 1행으로 본 것이다.
[상영]
| 상영번호 | 관이름 | 관객수 |
|---|---|---|
| 1 | A관 | 50 |
| 2 | A관 | 40 |
| 3 | A관 | 40 |
| 4 | A관 | 20 |
| 5 | B관 | 30 |
| 6 | B관 | (null) |
| 7 | B관 | 30 |
SELECT 관이름, 상영번호, 관객수,
( ㉠ ) OVER (PARTITION BY 관이름
ORDER BY 관객수 DESC) AS 순위
FROM 상영
ORDER BY 관이름, 순위, 상영번호;| 관이름 | 상영번호 | 관객수 | 순위 |
|---|---|---|---|
| A관 | 1 | 50 | 1 |
| A관 | 2 | 40 | 2 |
| A관 | 3 | 40 | 2 |
| A관 | 4 | 20 | 3 |
| B관 | 6 | (null) | 1 |
| B관 | 5 | 30 | 2 |
| B관 | 7 | 30 | 2 |
결과에서 A관은 1·2·2·3, B관은 1·2·2 로 동점은 같은 순위를 받고 다음 순위가 건너뛰지 않으므로 DENSE_RANK 이다.
B관은 DESC 정렬에서 NULL 이 맨 앞에 와 6번이 1위가 되고, 관객수 30 인 5·7번이 공동 2위가 된다.
- RANK 는 동점 다음 순위를 건너뛰어 A관 20 이 4위가 된다.
- ROW_NUMBER 는 동점에도 서로 다른 번호를 매겨 2·3 처럼 나뉜다.
- PERCENT_RANK 는 0 과 1 사이의 비율 값을 돌려준다.
[일별관객]
| 관이름 | 상영일자 | 관객수 |
|---|---|---|
| A관 | 2026-03-01 | 100 |
| A관 | 2026-03-02 | 80 |
| A관 | 2026-03-03 | 120 |
| B관 | 2026-03-01 | 50 |
| B관 | 2026-03-02 | (null) |
| B관 | 2026-03-03 | 70 |
SELECT 관이름, 상영일자,
SUM(관객수) OVER (PARTITION BY 관이름
ORDER BY 상영일자
ROWS BETWEEN 1 PRECEDING
AND CURRENT ROW) AS 이동합
FROM 일별관객
ORDER BY 관이름, 상영일자;| 관이름 | 상영일자 | 이동합 |
|---|---|---|
| A관 | 2026-03-01 | 100 |
| A관 | 2026-03-02 | 180 |
| A관 | 2026-03-03 | 200 |
| B관 | 2026-03-01 | 50 |
| B관 | 2026-03-02 | (null) |
| B관 | 2026-03-03 | (null) |
| 관이름 | 상영일자 | 이동합 |
|---|---|---|
| A관 | 2026-03-01 | 100 |
| A관 | 2026-03-02 | 180 |
| A관 | 2026-03-03 | 300 |
| B관 | 2026-03-01 | 50 |
| B관 | 2026-03-02 | 50 |
| B관 | 2026-03-03 | 120 |
| 관이름 | 상영일자 | 이동합 |
|---|---|---|
| A관 | 2026-03-01 | 100 |
| A관 | 2026-03-02 | 180 |
| A관 | 2026-03-03 | 200 |
| B관 | 2026-03-01 | 50 |
| B관 | 2026-03-02 | 50 |
| B관 | 2026-03-03 | 70 |
| 관이름 | 상영일자 | 이동합 |
|---|---|---|
| A관 | 2026-03-01 | 100 |
| A관 | 2026-03-02 | 180 |
| A관 | 2026-03-03 | 200 |
| B관 | 2026-03-01 | 170 |
| B관 | 2026-03-02 | 50 |
| B관 | 2026-03-03 | 70 |
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW 는 파티션 안에서 직전 1행과 현재 행만 더하는 이동 합이라 A관은 100·180·200 이다.
B관 둘째 날은 50 과 NULL 중 NULL 을 건너뛰어 50, 셋째 날은 NULL 과 70 이라 70 이 되며 첫 행은 직전 행이 없어 자기 값이다.
- 300·120 이 나온 표는 처음부터 누적한 합으로 본 결과이다.
- B관에 NULL 이 이어지는 표는 SUM 이 NULL 을 전파한다고 본 것이다.
- B관 첫 행이 170 인 표는 파티션을 무시하고 A관 마지막 행을 더한 것이다.
[예매]
| 예매번호 | 회원번호 | 금액 | 상태 |
|---|---|---|---|
| 1 | 7 | 15000 | 완료 |
| 2 | 7 | 12000 | 취소 |
| 3 | 7 | 9000 | 완료 |
| 4 | 8 | 20000 | 취소 |
| 5 | 8 | 11000 | 완료 |
| 6 | 8 | 11000 | 완료 |
| 7 | 9 | 8000 | 완료 |
SELECT 회원번호, 예매번호, 금액
FROM (SELECT 회원번호, 예매번호, 금액, 상태,
ROW_NUMBER() OVER (PARTITION BY 회원번호
ORDER BY 금액 DESC, 예매번호) AS RN
FROM 예매) T
WHERE RN <= 2
AND 상태 = '완료'
ORDER BY 회원번호, 예매번호;| 회원번호 | 예매번호 | 금액 |
|---|---|---|
| 7 | 1 | 15000 |
| 회원번호 | 예매번호 | 금액 |
|---|---|---|
| 7 | 1 | 15000 |
| 8 | 5 | 11000 |
| 9 | 7 | 8000 |
| 회원번호 | 예매번호 | 금액 |
|---|---|---|
| 7 | 1 | 15000 |
| 8 | 5 | 11000 |
| 8 | 6 | 11000 |
| 9 | 7 | 8000 |
| 회원번호 | 예매번호 | 금액 |
|---|---|---|
| 7 | 1 | 15000 |
| 7 | 3 | 9000 |
| 8 | 5 | 11000 |
| 8 | 6 | 11000 |
| 9 | 7 | 8000 |
ROW_NUMBER 는 인라인 뷰 안에서 취소 건까지 포함해 회원별로 매겨지고, 상태 조건은 그 뒤 바깥 WHERE 에서 적용되므로 결과는 3행이다.
회원 7 은 1·2위가 예매 1·2 인데 2 가 취소라 1 만, 회원 8 은 1위 4 가 취소라 2위 5 만, 회원 9 는 7 만 남는다.
- 5행 표는 취소 건을 먼저 거른 뒤 순번을 매긴 결과이다.
- 회원 8 이 두 행인 표는 보조 정렬 예매번호를 무시하고 동점 11000 을 같은 순위로 본 결과이다.
- 1행 표는 PARTITION BY 없이 전체 상위 2건만 본 결과이다.
[영화]
| 제목 | 관객수 |
|---|---|
| 파도 | 900 |
| 정원 | 700 |
| 추적 | 800 |
| 별빛 | 500 |
| 노을 | 600 |
㉠ SELECT 제목, 관객수
FROM (SELECT 제목, 관객수 FROM 영화
ORDER BY 관객수 DESC)
WHERE ROWNUM BETWEEN 2 AND 3;
㉡ SELECT 제목, 관객수
FROM 영화
WHERE ROWNUM <= 3 AND ROWNUM >= 2
ORDER BY 관객수 DESC;
㉢ SELECT 제목, 관객수
FROM (SELECT ROWNUM AS RN, A.*
FROM (SELECT 제목, 관객수 FROM 영화
ORDER BY 관객수 DESC) A)
WHERE RN BETWEEN 2 AND 3;
㉣ SELECT ROWNUM AS RN, 제목, 관객수
FROM (SELECT 제목, 관객수 FROM 영화
ORDER BY 관객수 DESC)
WHERE RN BETWEEN 2 AND 3;ROWNUM 은 조건을 통과한 행에 1부터 차례로 붙으므로 첫 행이 1 이 되지 못하면 다음 행도 1 이 되어 ROWNUM >= 2 류 조건은 0건이 된다.
㉢처럼 정렬한 인라인 뷰에 ROWNUM 을 별칭 RN 으로 고정한 뒤 바깥에서 RN 범위를 걸어야 추적·정원 두 행이 나온다.
- ㉠은 ROWNUM BETWEEN 2 AND 3 이라 첫 행부터 탈락해 결과가 없다.
- ㉡도 ROWNUM >= 2 를 만족하는 행이 없고 정렬도 번호 부여 뒤에 일어난다.
- ㉣은 같은 쿼리 블록의 WHERE 에서 SELECT 별칭 RN 을 쓸 수 없어 오류가 난다.
[장르분류]
| 분류코드 | 분류명 | 상위코드 |
|---|---|---|
| A | 영화 | (null) |
| A1 | 액션 | A |
| A2 | 드라마 | A |
| A11 | 첩보 | A1 |
SELECT LEVEL, 분류명 FROM 장르분류 START WITH 분류코드 = 'A1' CONNECT BY PRIOR 분류코드 = 상위코드;
PRIOR 분류코드 = 상위코드 는 앞서 읽은 행의 분류코드를 상위코드로 가진 자식을 찾으므로 순방향(부모→자식) 전개다.
결과는 시작 행 액션(LEVEL 1)과 그 하위 첩보(LEVEL 2) 두 행이며, 시작 행 위쪽은 탐색하지 않는다.
- LEVEL 은 시작 행을 1 로 세며 0 부터 시작하지 않는다.
- 역방향은
PRIOR 상위코드 = 분류코드처럼 PRIOR 를 자식 쪽 열에 둘 때이다. - 순방향이므로 시작 행의 상위인 영화는 결과에 나오지 않는다.
SELECT 분류명 FROM 장르분류 WHERE ( ㉠ ) START WITH 상위코드 IS NULL CONNECT BY PRIOR 분류코드 = 상위코드;
CONNECT_BY_ISLEAF 는 현재 행에 자식이 없으면 1, 있으면 0 을 돌려주므로 최하위 분류만 남기려면 = 1 조건을 쓴다.
계층형 질의의 WHERE 절은 계층 전개가 끝난 뒤 결과 행을 거르는 데 쓰인다.
- LEVEL = 1 은 START WITH 로 시작한 최상위 분류만 남긴다.
- 상위코드 IS NOT NULL 은 하위가 있는 중간 분류까지 함께 남긴다.
- ROWNUM = 1 은 전개 결과의 첫 행인 최상위 분류 하나만 남긴다.
[예매]
| 예매번호 | 제목 | 요일 | 인원 |
|---|---|---|---|
| 1 | 파도 | 토 | 2 |
| 2 | 파도 | 일 | 3 |
| 3 | 파도 | 일 | (null) |
| 4 | 정원 | 토 | 4 |
| 5 | 정원 | 금 | 1 |
| 6 | 추적 | 금 | 2 |
SELECT 제목,
SUM(CASE WHEN 요일 = '토' THEN 인원 ELSE 0 END) AS 토,
SUM(CASE WHEN 요일 = '일' THEN 인원 ELSE 0 END) AS 일
FROM 예매
GROUP BY 제목, 요일
ORDER BY 제목, 요일;| 제목 | 토 | 일 |
|---|---|---|
| 정원 | 4 | 0 |
| 추적 | 0 | 0 |
| 파도 | 2 | 3 |
| 제목 | 토 | 일 |
|---|---|---|
| 정원 | 0 | 0 |
| 정원 | 4 | 0 |
| 파도 | 0 | 3 |
| 파도 | 2 | 0 |
| 제목 | 토 | 일 |
|---|---|---|
| 정원 | 0 | 0 |
| 정원 | 4 | 0 |
| 추적 | 0 | 0 |
| 파도 | 0 | (null) |
| 파도 | 2 | 0 |
| 제목 | 토 | 일 |
|---|---|---|
| 정원 | 0 | 0 |
| 정원 | 4 | 0 |
| 추적 | 0 | 0 |
| 파도 | 0 | 3 |
| 파도 | 2 | 0 |
GROUP BY 에 피벗 대상인 요일까지 포함했으므로 (제목, 요일) 조합마다 한 행이 생겨 피벗되지 않고 5행이 된다.
정원은 금·토 두 행, 추적은 금 한 행(토·일 모두 0), 파도는 일 행에서 3, 토 행에서 2 가 나온다.
- 제목당 한 행인 표는 GROUP BY 에 제목만 둔 올바른 피벗 결과이다.
- 파도 일 칸이 NULL 인 표는 SUM 이 NULL 인원 행 때문에 NULL 이 된다고 본 것이다.
- 추적 행이 없는 표는 토·일 예매가 없는 그룹이 사라진다고 본 것이다.
[예매]
| 예매번호 | 좌석 |
|---|---|
| 1 | H-07 |
| 2 | J 12 |
| 3 | K1,K2 |
| 4 | VIP3 |
| 5 | A_5 |
| 6 | B12 |
SELECT 예매번호, 좌석 FROM 예매 WHERE REGEXP_LIKE(좌석, '[[:space:][:punct:]]');
[[:space:][:punct:]] 는 공백 또는 구두점 한 글자와 일치하는 문자 목록이고 앵커가 없어 어디에 있든 하나만 있으면 참이므로 4행이다.
하이픈의 H-07, 공백의 J 12, 쉼표의 K1,K2, 밑줄의 A_5 가 일치하고 영숫자뿐인 VIP3·B12 는 빠진다.
- 3행은 밑줄(_)을 구두점이 아니라고 본 값이다.
- 2행은 공백 클래스만 또는 구두점 중 일부만 적용한 값이다.
- 5행은 숫자가 섞인 VIP3 까지 일치한다고 본 값이다.
[요금표]
| 상영관 | 요금 |
|---|---|
| 1관 | 12000 |
| 2관 | 13000 |
| 3관 | 15000 |
[신규요금]
| 상영관 | 요금 |
|---|---|
| 2관 | 14000 |
| 3관 | 15000 |
| 4관 | 18000 |
MERGE INTO 요금표 T USING 신규요금 S ON (T.상영관 = S.상영관) WHEN MATCHED THEN UPDATE SET T.요금 = S.요금 WHEN NOT MATCHED THEN INSERT (상영관, 요금) VALUES (S.상영관, S.요금);
ON 조건에 맞는 2관·3관은 WHEN MATCHED 로 갱신되고 짝이 없는 4관은 WHEN NOT MATCHED 로 삽입되어 처리 행 수는 3, 요금표는 4행이 된다.
3관은 바뀌기 전후 요금이 같지만 조건에 맞았으므로 UPDATE 대상으로 처리되고, 원본에 없는 1관은 영향을 받지 않는다.
- 처리 2행은 값이 같은 3관을 갱신 대상에서 뺀 값이다.
- 요금표 3행은 4관이 삽입된다는 점을 놓친 값이다.
- 요금표 6행은 원본 행을 모두 새로 붙인다고 본 값이다.
-- 예매번호 10 의 인원은 처음에 2 이다 [세션 A] UPDATE 예매 SET 인원 = 5 WHERE 예매번호 = 10; [세션 B] SELECT 인원 FROM 예매 WHERE 예매번호 = 10; -- ㉠ [세션 A] COMMIT;
Oracle 은 읽기 일관성을 보장해 커밋되지 않은 다른 세션의 변경을 보여 주지 않으므로, 세션 B 는 변경 전 값 2 를 읽는다.
조회는 행 잠금을 기다리지 않고 언두(Undo) 정보로 변경 전 데이터를 만들어 돌려준다.
- 미커밋 값 5 를 읽는 것은 Dirty Read 로 Oracle 에서는 일어나지 않는다.
- 잠금은 같은 행을 수정하려는 DML 끼리만 기다리게 한다.
- 단순 조회는 잠금 충돌 오류를 일으키지 않는다.
CREATE TABLE 상영관 (관번호 INT PRIMARY KEY, 좌석수 INT);
-- 상영관: (1, 100), (2, 80)
CREATE TABLE 상영 (
상영번호 INT PRIMARY KEY,
관번호 INT REFERENCES 상영관(관번호),
시작시각 CHAR(5) NOT NULL,
요금 INT DEFAULT 10000
CHECK (요금 BETWEEN 5000 AND 20000),
UNIQUE (관번호, 시작시각)
);
-- 상영: (1, 1, '10:00', 12000)
㉠ INSERT INTO 상영 VALUES (2, NULL, '10:00', 9000);
㉡ INSERT INTO 상영 (상영번호, 관번호, 시작시각)
VALUES (3, 2, '10:00');
㉢ INSERT INTO 상영 VALUES (4, 1, '13:00', 20000);
㉣ INSERT INTO 상영 VALUES (5, 1, '10:00', 8000);㉣은 관번호 1·시작시각 '10:00' 조합이 기존 상영번호 1 행과 같아 복합 UNIQUE 제약 (관번호, 시작시각) 을 위반한다.
복합 UNIQUE 는 두 열 값이 모두 같을 때만 막으며, 한 열만 같거나 NULL 이 섞인 행은 허용한다.
- ㉠은 FK 열이 NULL 이라 참조 검사를 하지 않고, UNIQUE 도 NULL 이 섞여 통과한다.
- ㉡은 생략한 요금에 DEFAULT 10000 이 들어가 CHECK 를 만족한다.
- ㉢의 20000 은 BETWEEN 의 끝값이라 포함되어 통과한다.
CREATE TABLE 영화 ( 영화번호 NUMBER PRIMARY KEY, 제목 VARCHAR2(50) ); CREATE TABLE 상영 ( 상영번호 NUMBER PRIMARY KEY, 영화번호 NUMBER REFERENCES 영화(영화번호), 상영일시 DATE );
영화 테이블의 기본키를 상영 테이블의 외래키가 참조하고 있으므로, 옵션 없이 부모인 영화를 DROP 하면 참조 제약 때문에 오류(ORA-02449)가 난다.
PURGE 는 휴지통을 거치지 않고 바로 지우라는 뜻일 뿐 참조 제약 검사는 그대로 받으며, 부모를 지우려면 CASCADE CONSTRAINTS 로 자식의 FK 제약을 함께 없애야 한다.
- 상영 테이블의 일반 열인 상영일시는 제약과 무관해 DROP COLUMN 으로 지울 수 있다.
- 자식 테이블을 TRUNCATE 하는 것은 참조 제약에 걸리지 않는다.
- CASCADE CONSTRAINTS 를 붙이면 상영의 FK 제약이 삭제되며 영화가 지워진다.
GRANT CREATE TABLE TO 매니저 WITH ( ㉠ ) OPTION;
CREATE TABLE 같은 시스템 권한을 재부여할 수 있게 주려면 WITH ADMIN OPTION 을 붙인다.
SELECT ON 테이블 같은 오브젝트 권한을 재부여 가능하게 줄 때는 WITH GRANT OPTION 을 쓴다.
- WITH GRANT OPTION 은 오브젝트 권한에만 쓰는 옵션이다.
- WITH CHECK OPTION 은 뷰를 통한 입력·수정을 뷰 조건으로 제한한다.
- WITH CASCADE OPTION 이라는 권한 부여 구문은 없다.