업무 처리가 데이터에 미치는 영향을 함께 보는 것은 데이터와 프로세스의 상관 관점(Interaction)이다.
무엇을 저장할지는 데이터 관점(What), 업무가 무엇을 하는지는 프로세스 관점(How)에서 본다.
- 데이터 관점은 데이터 사이의 관계만 분석하고 처리의 영향은 보지 않는다.
- 프로세스 관점은 업무 처리 방식 자체에 집중한다.
- 물리 저장 구조는 모델링의 세 관점에 속하지 않는다.
업무 수행 중 발생하는 행위를 기록하는 탑승 수속은 사건 엔터티(Event Entity)다.
유무형에 따라 물리적 실체가 있는 유형 엔터티, 관리할 개념인 개념 엔터티, 업무 행위로 생기는 사건 엔터티로 나뉜다.
- 항공기는 물리적 형태가 있는 유형 엔터티다.
- 운임 규정은 형태 없이 관리되는 개념 엔터티다.
- 승무원은 실체가 있는 유형 엔터티다.
여러 구간 운임을 합산해 얻는 총운임은 다른 속성에서 값이 도출되는 파생 속성(Derived Attribute)이다.
파생 속성은 원천 값이 바뀔 때 함께 갱신해야 하므로 꼭 필요한 경우에만 둔다.
- 시스템이 부여한 예약번호는 설계 속성이다.
- 승객이 입력한 여권 성명은 업무에서 바로 얻는 기본 속성이다.
- 등급 코드는 업무 편의를 위해 새로 정한 설계 속성이다.
IE 표기법에서 관계선 끝의 원(○)은 선택 참여, 바(|)는 필수 참여를 뜻하므로 예약 쪽 끝에 원이 있는 승객은 예약 없이도 존재할 수 있다.
승객 쪽 끝은 바로 표시돼 예약은 반드시 한 승객에 속하고, 예약과 탑승권은 까마귀발이 없는 1:1 관계다.
- 예약은 승객 쪽이 필수라 승객 없이 등록될 수 없다.
- 탑승권 쪽에 까마귀발이 없어 예약당 탑승권은 최대 1장이다.
- 항공편에서 본 예약 쪽 끝이 원이라 예약이 0건인 항공편도 있다.
식별 관계를 계속 이어 가면 부모의 주식별자가 자식에게 상속되어 하위로 갈수록 주식별자 속성 수가 늘어난다.
수하물의 PK 에 편명·예약번호·탑승권번호가 모두 들어가 조인 조건과 SQL 이 복잡해지므로 적절한 곳에서 비식별 관계로 끊는다.
- 부모 없이 자식을 만들 수 있는 것은 비식별 관계의 특징이다.
- 상속된 식별자 덕분에 상위 엔터티와의 조인은 오히려 쉬워진다.
- 주식별자에는 NULL 이 허용되지 않는다.
반정규화는 반정규화 대상 조사 → 다른 방법 유도 검토(뷰·인덱스·클러스터링·응용 로직) → 반정규화 적용 순으로 진행한다.
예약에 출발 도시를 중복 두면 조인은 줄지만 공항의 도시가 바뀔 때마다 함께 고쳐야 하는 정합성 부담이 생긴다.
- 중복 속성은 데이터 정합성 관리를 더 필요하게 만든다.
- 반정규화는 무결성을 일부 희생해 조회 성능을 얻는 기법이다.
- 공항을 삭제하면 공항 정보 자체를 관리할 수 없게 된다.
관계는 자식의 외래키(FK) 값을 부모의 기본키(PK) 값과 같다고 비교하는 조인 조건으로 구현된다.
1:N 관계에서는 승객 한 명이 그 승객의 예약 수만큼 반복되어 나타난다.
- 조인은 값 비교로 수행되므로 FK 제약이 선언되지 않아도 가능하다.
- 결과 행 수는 승객 수가 아니라 조건을 만족한 예약 수를 따른다.
- 예약이 여러 건인 승객은 승객번호가 여러 번 나온다.
예약 행을 만들고 항공편의 잔여 좌석을 줄이는 작업은 하나만 반영되면 좌석 수가 틀어지므로 하나의 트랜잭션으로 묶어 원자성을 보장해야 한다.
함께 변경돼야 하는 엔터티(예약·항공편)는 같은 업무 단위로 묶어 모두 반영하거나 모두 취소한다.
- 승객 정보 조회는 데이터를 바꾸지 않는다.
- 공항 코드 목록 출력은 단순 조회다.
- 실적 집계 조회도 변경이 없어 원자성 문제가 생기지 않는다.
COUNT(*)는 NULL 여부와 관계없이 모든 행을 세어 10이고, COUNT(수하물무게)는 NULL 을 제외한 7이다.
NULL 이 포함된 산술 연산 결과는 NULL 이며 AVG 는 NULL 이 아닌 7개 값의 합을 7로 나눈다.
- COUNT(*)는 행 자체를 세므로 NULL 이 있는 행도 포함한다.
- NULL + 5 는 5가 아니라 NULL 이다.
- AVG 의 분모는 10이 아니라 NULL 을 뺀 7이다.
인조식별자는 행마다 새 값이 붙어 업무상 같은 탑승권도 다른 행으로 저장되므로, 본질 속성 조합(편명·출발일·좌석번호)에 UNIQUE 제약을 두어 업무 중복을 막는다.
인조식별자 PK 는 조인 편의를 위해 그대로 두고 본질식별자의 유일성은 별도 제약으로 보장한다.
- 일련번호 길이를 늘려도 업무 중복은 그대로 들어온다.
- NOT NULL 은 값의 존재만 강제할 뿐 중복을 막지 않는다.
- 무작위 문자열도 행마다 다른 값이라 중복을 막지 못한다.
TRUNCATE는 테이블 구조를 남기고 데이터를 모두 지우는 DDL이며 Oracle 에서는 자동 커밋되어 ROLLBACK 할 수 없다.
MERGE 는 DML, SAVEPOINT 는 TCL, REVOKE 는 DCL 에 속한다.
- MERGE 는 행을 입력하거나 수정하는 DML 이다.
- SAVEPOINT 는 트랜잭션 저장점을 지정하는 TCL 이다.
- REVOKE 는 권한을 회수하는 DCL 이다.
SELECT DISTINCT 등급, 운임 * 2 + 100 AS "왕복 운임" FROM 예약;
DISTINCT는 SELECT 목록 전체, 즉 등급과 왕복 운임의 조합이 같은 행을 하나로 합친다.
산술식은 곱셈을 먼저 하므로 운임의 두 배에 100을 더하며, 공백이 든 별칭은 큰따옴표로 감싸야 한다.
- 곱셈이 먼저라 운임에 102를 곱하지 않는다.
- 큰따옴표를 빼면 공백 때문에 구문 오류가 난다.
- DISTINCT 는 특정 열 하나에만 걸리지 않는다.
[예약]
| 예약번호 | 편명 | 등급 | 운임 | 상태 |
|---|---|---|---|---|
| R01 | KE101 | Y | 200 | 확정 |
| R02 | KE101 | Y | 220 | 취소 |
| R03 | KE101 | C | 500 | 확정 |
| R04 | OZ202 | Y | 180 | 확정 |
| R05 | OZ202 | Y | 190 | 확정 |
| R06 | OZ202 | C | 450 | 취소 |
| R07 | KE303 | Y | 210 | 확정 |
| R08 | KE303 | Y | 210 | 취소 |
| R09 | KE303 | Y | 230 | 취소 |
SELECT 편명, COUNT(*) AS 건수, SUM(운임) AS 합계 FROM 예약 WHERE 상태 = '확정' GROUP BY 편명 HAVING COUNT(*) >= 2 ORDER BY 합계 DESC;
| 편명 | 건수 | 합계 |
|---|---|---|
| OZ202 | 2 | 370 |
| KE101 | 2 | 700 |
| 편명 | 건수 | 합계 |
|---|---|---|
| KE101 | 2 | 700 |
| OZ202 | 2 | 370 |
| 편명 | 건수 | 합계 |
|---|---|---|
| KE101 | 2 | 700 |
| OZ202 | 2 | 370 |
| KE303 | 1 | 210 |
| 편명 | 건수 | 합계 |
|---|---|---|
| KE101 | 3 | 920 |
| OZ202 | 3 | 820 |
| KE303 | 3 | 650 |
논리적 처리 순서는 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY이므로 취소 행이 먼저 걸러진 뒤 확정 예약만으로 편명별 건수를 센다.
확정이 1건뿐인 KE303 은 HAVING 에서 빠지고, 남은 두 행은 SELECT 에서 만든 별칭 합계로 내림차순 정렬된다.
- KE303 이 남은 표는 HAVING 건수를 취소 포함 전체 행으로 판정했다.
- OZ202 가 먼저 온 표는 정렬 방향을 오름차순으로 보았다.
- 건수가 3인 표는 WHERE 필터를 무시했다.
[승객]
| 승객번호 | 마일리지 |
|---|---|
| P1 | 120000 |
| P2 | 50000 |
| P3 | (null) |
| P4 | 80000 |
| P5 | 30000 |
SELECT 승객번호,
CASE WHEN 마일리지 >= 50000 THEN '골드'
WHEN 마일리지 >= 100000 THEN '플래티넘'
WHEN 마일리지 < 50000 THEN '일반'
ELSE '미가입' END AS 등급
FROM 승객
ORDER BY 승객번호;| 승객번호 | 등급 |
|---|---|
| P1 | 골드 |
| P2 | 골드 |
| P3 | 일반 |
| P4 | 골드 |
| P5 | 일반 |
| 승객번호 | 등급 |
|---|---|
| P1 | 골드 |
| P2 | 골드 |
| P3 | 미가입 |
| P4 | 골드 |
| P5 | 일반 |
| 승객번호 | 등급 |
|---|---|
| P1 | 플래티넘 |
| P2 | 골드 |
| P3 | 미가입 |
| P4 | 골드 |
| P5 | 일반 |
| 승객번호 | 등급 |
|---|---|
| P1 | 골드 |
| P2 | 일반 |
| P3 | 미가입 |
| P4 | 골드 |
| P5 | 일반 |
검색형 CASE는 WHEN 을 위에서부터 평가해 처음 참이 되는 조건의 값을 돌려주므로 120000 인 P1 도 첫 조건(50000 이상)에 걸려 골드가 된다.
마일리지가 NULL 인 P3 은 모든 비교가 UNKNOWN 이라 ELSE 의 미가입이 된다.
- P1 을 플래티넘으로 본 표는 가장 잘 맞는 조건을 고른다고 오해했다.
- P3 을 일반으로 본 표는 NULL < 50000 을 참으로 보았다.
- P2 를 일반으로 본 표는 50000 이 50000 이상 조건을 만족함을 놓쳤다.
SELECT SUBSTR('ICN-NRT-ICN', 5, 3) AS 부분,
INSTR('ICN-NRT-ICN', 'ICN', 2) AS 위치,
CEIL(-2.3) AS 올림,
FLOOR(-2.3) AS 내림
FROM DUAL;| 부분 | 위치 | 올림 | 내림 |
|---|---|---|---|
| NRT | 9 | -2 | -3 |
| 부분 | 위치 | 올림 | 내림 |
|---|---|---|---|
| NRT | 9 | -3 | -2 |
| 부분 | 위치 | 올림 | 내림 |
|---|---|---|---|
| RT- | 9 | -2 | -3 |
| 부분 | 위치 | 올림 | 내림 |
|---|---|---|---|
| NRT | 1 | -2 | -3 |
SUBSTR는 5번째 글자부터 3글자를 잘라 NRT 를, INSTR는 2번째 글자부터 ICN 을 찾아 뒤쪽 ICN 의 시작 위치 9를 돌려준다.
CEIL은 그 값 이상인 최소 정수라 -2, FLOOR 는 그 값 이하인 최대 정수라 -3 이 된다.
- 위치가 1인 표는 시작 위치 2를 무시하고 처음부터 찾았다.
- 올림과 내림이 뒤바뀐 표는 음수에서 방향을 반대로 보았다.
- 부분이 RT- 인 표는 시작 위치를 한 칸 밀려 셌다.
[탑승]
| 탑승권번호 | 지정좌석 | 변경좌석 |
|---|---|---|
| B01 | 12A | (null) |
| B02 | 14C | 3F |
| B03 | (null) | (null) |
| B04 | (null) | 22B |
| B05 | 30A | 30A |
SELECT 탑승권번호,
( ㉠ ) AS 최종좌석
FROM 탑승
ORDER BY 탑승권번호;| 탑승권번호 | 최종좌석 |
|---|---|
| B01 | 12A |
| B02 | 3F |
| B03 | 미배정 |
| B04 | 22B |
| B05 | 30A |
COALESCE 는 인자를 왼쪽부터 보아 처음 나온 NULL 이 아닌 값을 돌려주므로 변경좌석, 지정좌석, 미배정 순서로 나열해야 결과표와 맞는다.
B02·B04 는 변경좌석, B01·B05 는 지정좌석, 둘 다 없는 B03 은 미배정이 된다.
- 지정좌석을 먼저 보는 NVL 중첩은 B02 를 14C 로 낸다.
- NULLIF 는 두 값이 같으면 NULL 을 돌려줄 뿐 대체값을 고르지 않는다.
- NVL2 는 변경좌석이 있을 때 지정좌석을 내므로 B02 가 14C 가 된다.
WHERE NOT 상태 = '취소' AND 운임 > 300
NOT 이 AND 에 앞서 평가되므로 조건은 (NOT 상태 = '취소') AND 운임 > 300 과 같다.
따라서 취소가 아닌 예약 가운데 운임이 300 을 넘는 행만 남는다.
- NOT 이 AND 전체에 걸린다고 보면 드모르간 법칙으로 OR 조건이 된다.
- 취소이면서 300 이하인 행은 두 조건을 모두 뒤집은 것이다.
- 둘 다 아닌 행은 NOT 이 OR 전체에 걸린 경우다.
NOT IN 은 목록의 모든 값과의 <> 비교를 AND 로 묶은 것이라 NULL 과의 비교가 UNKNOWN 이 되어 어떤 행도 선택되지 않는다.
IN 은 OR 로 풀리므로 NULL 이 섞여도 다른 값과 일치하는 행은 나오지만 NULL 행 자체는 = 비교로 찾을 수 없다.
- BETWEEN a AND b 는 a 이상 b 이하라 하한이 더 크면 결과가 없다.
- LIKE '%' 도 NULL 과 비교하면 UNKNOWN 이라 NULL 행은 빠진다.
- NULL 행을 찾으려면 IS NULL 을 써야 한다.
[탑승]
| 탑승권번호 | 편명 | 등급 |
|---|---|---|
| T01 | KE101 | Y |
| T02 | KE101 | Y |
| T03 | KE101 | C |
| T04 | KE101 | (null) |
| T05 | OZ202 | Y |
| T06 | OZ202 | (null) |
| T07 | OZ202 | (null) |
| T08 | KE303 | C |
| T09 | KE303 | C |
| T10 | KE303 | Y |
SELECT 편명, 등급, COUNT(*) AS 건수 FROM 탑승 GROUP BY 편명, 등급 HAVING COUNT(등급) >= 1 ORDER BY 편명, 등급;
GROUP BY 편명, 등급은 NULL 등급도 하나의 그룹으로 만들어 7개 그룹이 생기지만, COUNT(등급)은 NULL 을 세지 않아 NULL 그룹은 0 이 되어 HAVING 에서 빠진다.
KE101-Y·KE101-C·OZ202-Y·KE303-C·KE303-Y 의 5행이 남는다.
- 7은 HAVING 조건을 COUNT(*)로 본 그룹 수다.
- 3은 편명 수만 센 값이다.
- 10은 그룹화 전 원본 행 수다.
[적립내역]
| 적립번호 | 등급 | 적립 |
|---|---|---|
| M1 | 골드 | 100 |
| M2 | 골드 | (null) |
| M3 | 골드 | 200 |
| M4 | 일반 | 60 |
| M5 | 일반 | (null) |
| M6 | 일반 | (null) |
| M7 | 실버 | (null) |
SELECT 등급,
COUNT(적립) AS C,
AVG(적립) AS A1,
SUM(적립) / COUNT(*) AS A2
FROM 적립내역
GROUP BY 등급
ORDER BY 등급;| 등급 | C | A1 | A2 |
|---|---|---|---|
| 골드 | 2 | 150 | 150 |
| 실버 | 0 | (null) | (null) |
| 일반 | 1 | 60 | 60 |
| 등급 | C | A1 | A2 |
|---|---|---|---|
| 골드 | 2 | 100 | 100 |
| 실버 | 0 | (null) | (null) |
| 일반 | 1 | 20 | 20 |
| 등급 | C | A1 | A2 |
|---|---|---|---|
| 골드 | 2 | 150 | 100 |
| 실버 | (null) | (null) | (null) |
| 일반 | 1 | 60 | 20 |
| 등급 | C | A1 | A2 |
|---|---|---|---|
| 골드 | 2 | 150 | 100 |
| 실버 | 0 | (null) | (null) |
| 일반 | 1 | 60 | 20 |
AVG(적립)은 NULL 을 뺀 값만 평균해 골드 150, 일반 60 이지만, SUM(적립)/COUNT(*)는 NULL 행까지 분모에 넣어 골드 100, 일반 20 이 된다.
값이 모두 NULL 인 실버는 COUNT(적립)이 0, SUM 이 NULL 이라 나눗셈 결과도 NULL 이다.
- 실버의 C 를 NULL 로 본 표는 COUNT 가 0 을 돌려준다는 점을 놓쳤다.
- 골드 평균을 100 으로 본 표는 AVG 가 NULL 을 0 으로 센다고 오해했다.
- A2 를 A1 과 같게 본 표는 COUNT(*)가 NULL 행도 센다는 점을 놓쳤다.
GROUP BY 없이 집계 함수를 쓰면 전체가 한 그룹이 되므로 SELECT 절에는 집계 결과나 상수만 올 수 있고, 집계되지 않은 편명 같은 일반 열을 함께 쓰면 오류가 난다.
'KE' 같은 상수는 모든 행에서 같은 값이라 집계 함수와 함께 써도 된다.
- 상수 별칭을 함께 쓴 SQL 은 정상 실행된다.
- MAX 와 MIN 의 차는 집계 결과끼리의 연산이라 가능하다.
- COUNT(DISTINCT 편명)은 중복을 뺀 편명 수를 센다.
[운항]
| 편명 | 지연분 |
|---|---|
| KE1 | 15 |
| KE2 | (null) |
| OZ3 | 0 |
| OZ4 | 15 |
| 7C5 | (null) |
| KE6 | 40 |
SELECT 편명, 지연분 FROM 운항 ORDER BY 지연분 DESC NULLS LAST, 편명 DESC;
| 편명 | 지연분 |
|---|---|
| KE6 | 40 |
| OZ4 | 15 |
| KE1 | 15 |
| OZ3 | 0 |
| KE2 | (null) |
| 7C5 | (null) |
| 편명 | 지연분 |
|---|---|
| KE6 | 40 |
| KE1 | 15 |
| OZ4 | 15 |
| OZ3 | 0 |
| KE2 | (null) |
| 7C5 | (null) |
| 편명 | 지연분 |
|---|---|
| OZ3 | 0 |
| OZ4 | 15 |
| KE1 | 15 |
| KE6 | 40 |
| KE2 | (null) |
| 7C5 | (null) |
| 편명 | 지연분 |
|---|---|
| KE2 | (null) |
| 7C5 | (null) |
| KE6 | 40 |
| OZ4 | 15 |
| KE1 | 15 |
| OZ3 | 0 |
NULLS LAST 로 NULL 을 맨 뒤로 보낸 채 지연분 내림차순(40, 15, 0)으로 놓고, 같은 지연분은 편명 DESC로 정렬해 OZ4 가 KE1 앞에 온다.
NULL 두 행도 편명 내림차순이라 KE2, 7C5 순이며 NULLS LAST 가 없으면 Oracle 은 DESC 에서 NULL 을 맨 앞에 둔다.
- NULL 이 맨 앞인 표는 NULLS LAST 를 무시한 기본 동작이다.
- KE1 이 OZ4 앞인 표는 두 번째 키를 오름차순으로 보았다.
- 0 부터 시작한 표는 지연분을 오름차순으로 정렬했다.
SELECT A.편명 AS 첫편, B.편명 AS 연결편
FROM 항공편 A
JOIN 항공편 B
ON A.도착공항 = B.출발공항;셀프 조인(Self Join)은 한 테이블을 별칭 A·B 로 두 번 참조해 서로 다른 테이블처럼 조인하며, 여기서는 A 의 도착공항에서 출발하는 B 를 찾아 연결편을 만든다.
조건을 만족하는 짝이 여럿이면 결과 행 수가 원래 행 수를 넘을 수 있다.
- 같은 테이블을 두 번 쓰므로 열을 구분할 별칭이 꼭 필요하다.
- 셀프 조인에도 LEFT OUTER JOIN 을 쓸 수 있다.
- 한 항공편이 여러 편과 연결되면 행 수가 늘어난다.
[예약]
| 예약번호 | 노선 |
|---|---|
| R1 | ICN-NRT |
| R2 | ICN-NRT |
| R3 | GMP-CJU |
| R4 | (null) |
| R5 | ICN-LAX |
[운임표]
| 노선 | 등급 | 운임 |
|---|---|---|
| ICN-NRT | Y | 300 |
| ICN-NRT | C | 900 |
| GMP-CJU | Y | 80 |
| GMP-CJU | C | 150 |
| GMP-CJU | F | 300 |
| ICN-HND | Y | 280 |
SELECT R.예약번호, F.등급, F.운임
FROM 예약 R
JOIN 운임표 F
ON R.노선 = F.노선
ORDER BY R.예약번호, F.등급;EQUI JOIN 은 노선 값이 같은 모든 짝을 만들므로 예약 한 건이 운임표의 같은 노선 행 수만큼 반복된다.
R1·R2 는 ICN-NRT 2행씩, R3 은 GMP-CJU 3행과 결합해 2+2+3=7행이고, 노선이 NULL 인 R4 와 운임표에 없는 R5 는 빠진다.
- 5는 예약 한 건당 한 행만 나온다고 본 값이다.
- 3은 매칭된 예약 건수만 센 값이다.
- 9는 매칭되지 않은 예약까지 한 행씩 더한 값이다.
[항공편]
| 편명 | 출발공항 |
|---|---|
| KE1 | ICN |
| KE2 | GMP |
| OZ3 | ICN |
| 7C4 | PUS |
[예약]
| 예약번호 | 편명 | 상태 |
|---|---|---|
| R1 | KE1 | 확정 |
| R2 | KE1 | 취소 |
| R3 | KE1 | 확정 |
| R4 | OZ3 | 취소 |
| R5 | KE2 | 확정 |
SELECT A.편명, B.예약번호 FROM 항공편 A, 예약 B WHERE A.편명 = B.편명(+) AND B.상태(+) = '확정' ORDER BY A.편명, B.예약번호;
(+) 는 NULL 이 채워질 쪽(예약)에 붙이며, B.상태(+) = '확정'처럼 필터에도 붙이면 LEFT OUTER JOIN 의 ON 절에 A.편명 = B.편명 AND B.상태 = '확정'을 둔 것과 같다.
KE1 확정 2행, KE2 1행에 확정 예약이 없는 OZ3·7C4 가 NULL 로 1행씩 보존되어 5행이다.
- 3은 상태 조건의 (+) 를 빠뜨려 내부 조인처럼 된 경우다.
- 6은 상태 조건을 무시해 OZ3 의 취소 예약까지 결합한 값이다.
- 20은 조인 조건이 없는 CROSS JOIN(4×5)의 행 수다.
[예약]
| 예약번호 | 좌석 |
|---|---|
| 1 | 12A |
| 2 | 12B |
| 3 | (null) |
| 4 | 14C |
[탑승]
| 탑승번호 | 좌석 |
|---|---|
| 91 | 12A |
| 92 | (null) |
| 93 | 15D |
| 94 | 12B |
SELECT R.예약번호, B.탑승번호
FROM 예약 R FULL OUTER JOIN 탑승 B
ON R.좌석 = B.좌석
ORDER BY R.예약번호, B.탑승번호;| 예약번호 | 탑승번호 |
|---|---|
| 1 | 91 |
| 2 | 94 |
| 3 | 92 |
| 4 | (null) |
| (null) | 93 |
| 예약번호 | 탑승번호 |
|---|---|
| 1 | 91 |
| 2 | 94 |
| 3 | (null) |
| 4 | (null) |
| 예약번호 | 탑승번호 |
|---|---|
| 1 | 91 |
| 2 | 94 |
| 3 | (null) |
| 4 | (null) |
| (null) | 92 |
| (null) | 93 |
| 예약번호 | 탑승번호 |
|---|---|
| 1 | 91 |
| 1 | 91 |
| 2 | 94 |
| 2 | 94 |
| 3 | (null) |
| 4 | (null) |
| (null) | 92 |
| (null) | 93 |
FULL OUTER JOIN 은 좌석이 같은 1-91, 2-94 두 쌍에 더해 양쪽에서 짝을 찾지 못한 행을 모두 NULL 과 결합해 남기므로 6행이다.
좌석이 NULL 인 예약 3과 탑승 92는 NULL = NULL 이 참이 아니므로 서로 매칭되지 않고, 각각 상대 열이 NULL 인 행으로 따로 나온다.
- 3과 92를 한 행으로 묶은 표는 NULL 끼리 같다고 본 결과이다.
- 매칭 행을 두 번씩 넣은 표는 LEFT 와 RIGHT 결과를 중복 제거 없이 더한 것이다.
- 탑승 92·93이 빠진 표는 LEFT OUTER JOIN 의 결과이다.
[항공편]
| 편명 | 출발공항 |
|---|---|
| KE101 | GMP |
| KE103 | GMP |
| OZ201 | ICN |
| LJ301 | GMP |
| TW401 | ICN |
[예약]
| 예약번호 | 편명 | 운임 |
|---|---|---|
| 1 | KE101 | 120 |
| 2 | KE101 | 95 |
| 3 | OZ201 | 300 |
| 4 | LJ301 | (null) |
| 5 | OZ201 | 280 |
SELECT F.편명, COUNT(R.예약번호) AS 예약수
FROM 항공편 F LEFT OUTER JOIN 예약 R
ON F.편명 = R.편명
( ㉠ )
GROUP BY F.편명
ORDER BY F.편명;[결과]
| 편명 | 예약수 |
|---|---|
| KE101 | 2 |
| KE103 | 0 |
| LJ301 | 1 |
기준 테이블인 항공편의 행을 거르려면 조건을 WHERE 절에 두어야 하며, 그러면 GMP 출발 3개 편만 남고 예약이 없는 KE103 도 예약수 0으로 나온다.
외부 조인의 ON 절에 둔 조건은 매칭 여부만 정할 뿐 왼쪽 행을 지우지 않는다. LJ301 은 운임이 NULL 이어도 예약번호가 있어 1로 센다.
- ON 절에 출발공항 조건을 두면 ICN 편도 예약수 0인 행으로 남아 5행이 된다.
- ON 절에 운임 조건을 두면 5개 편이 모두 남고 LJ301 의 예약수가 0이 된다.
- WHERE 절에 운임 조건을 두면 예약이 있는 KE101·OZ201 두 행만 남는다.
[예약]
| 예약번호 | 승객명 | 편명 |
|---|---|---|
| 1 | 김하늘 | KE101 |
| 2 | 이하람 | OZ330 |
| 3 | 박구름 | (null) |
| 4 | 최나래 | KE205 |
| 5 | 정보라 | LJ007 |
| 6 | 윤솔 | KE101 |
[결항편]
| 편명 |
|---|
| OZ330 |
| LJ007 |
SELECT 예약번호, 승객명 FROM 예약 WHERE 편명 NOT IN (SELECT 편명 FROM 결항편) ORDER BY 예약번호;
| 예약번호 | 승객명 |
|---|---|
| 2 | 이하람 |
| 5 | 정보라 |
| 예약번호 | 승객명 |
|---|---|
| 1 | 김하늘 |
| 4 | 최나래 |
| 6 | 윤솔 |
| 예약번호 | 승객명 |
|---|
| 예약번호 | 승객명 |
|---|---|
| 1 | 김하늘 |
| 3 | 박구름 |
| 4 | 최나래 |
| 6 | 윤솔 |
편명이 결항편 목록에 없는 예약 1·4·6만 남고, 편명이 NULL 인 예약 3은 비교 결과가 UNKNOWN 이라 NOT IN 조건을 통과하지 못한다.
NULL 과의 비교는 참도 거짓도 아니므로 NULL 행은 IN 과 NOT IN 어느 쪽에도 나오지 않는다. 서브쿼리 쪽에는 NULL 이 없으므로 나머지 행은 정상 비교된다.
- 박구름까지 포함한 표는 NOT EXISTS 처럼 NULL 행을 남긴 결과이다.
- 이하람·정보라만 있는 표는 IN 조건의 결과이다.
- 행이 없는 표는 서브쿼리 결과에 NULL 이 있을 때의 상황이다.
[국내예약]
| 예약번호 | 운임 |
|---|---|
| 101 | 120 |
| 102 | 350 |
| 103 | 480 |
| 104 | 600 |
| 105 | 300 |
[국제예약]
| 예약번호 | 운임 |
|---|---|
| 201 | 300 |
| 202 | 450 |
| 203 | 520 |
SELECT D.예약번호, D.운임
FROM 국내예약 D
WHERE EXISTS (SELECT 1
FROM 국제예약 I
WHERE ( ㉠ ))
ORDER BY D.예약번호;[결과]
| 예약번호 | 운임 |
|---|---|
| 102 | 350 |
| 103 | 480 |
| 104 | 600 |
국제 운임 중 하나라도 자기 운임 미만인 것이 있으면 참이 되므로, 이 EXISTS 는 D.운임 > ANY 와 같은 뜻의 최솟값 300 초과 조건이다.
350·480·600 은 300 을 넘어 남고, 120 은 모든 국제 운임 미만이며 300 은 300 초과가 아니라서 빠진다.
- D.운임 >= I.운임 은 >= ANY 와 같아 운임 300인 예약 105까지 들어온다.
- I.운임 - D.운임 > 0 은 520 미만인 국내 운임 네 건을 고르는 < ANY 와 같다.
- 같음 조건은 IN 과 같아 운임 300인 예약 105 한 행만 나온다.
SELECT P.승객명, P.마일리지
FROM 승객 P
WHERE P.마일리지 > (SELECT AVG(Q.마일리지)
FROM 승객 Q
WHERE Q.등급 = P.등급);서브쿼리가 바깥 행의 P.등급 을 참조하는 상관 서브쿼리이므로, 바깥 행마다 같은 등급 승객의 평균을 다시 계산해 비교한다.
결과적으로 마일리지가 자기 등급 평균을 넘는 승객만 남고, 등급이 NULL 이면 Q.등급 = NULL 이 참이 되지 않아 평균이 NULL 이고 비교도 UNKNOWN 이 된다.
- 바깥 값에 따라 결과가 달라지므로 한 번 실행으로 끝나지 않는다.
- 등급이 NULL 인 승객은 평균이 NULL 이 되어 결과에서 빠진다.
- 서브쿼리 안 별칭 Q 는 메인 쿼리에서 참조할 수 없다.
UNION 은 두 결과 사이뿐 아니라 한 SELECT 결과 안의 중복 행도 하나로 줄이므로, 결과 행 수가 어느 한쪽의 원래 행 수보다 적을 수 있다.
예를 들어 같은 행 5개인 결과와 다른 행 1개인 결과를 UNION 하면 2행만 남는다.
- UNION ALL 은 중복을 그대로 두므로 행 수가 양쪽 행 수의 합과 같다.
- 중복 제거 작업이 없어 중복이 없음이 확실하면 UNION ALL 이 유리하다.
- UNION 은 UNION ALL 결과에 DISTINCT 를 적용한 것처럼 중복 행을 없앤다.
[예약]
| 승객번호 | 편명 |
|---|---|
| 1 | KE101 |
| 1 | KE101 |
| 1 | KE205 |
| 2 | KE101 |
| 3 | OZ330 |
| 3 | OZ330 |
| 4 | (null) |
[탑승]
| 승객번호 | 편명 |
|---|---|
| 1 | KE101 |
| 2 | KE205 |
| 3 | OZ330 |
| 4 | (null) |
SELECT 승객번호, 편명 FROM 예약 MINUS SELECT 승객번호, 편명 FROM 탑승 ORDER BY 승객번호;
| 승객번호 | 편명 |
|---|---|
| 1 | KE205 |
| 2 | KE101 |
| 4 | (null) |
| 승객번호 | 편명 |
|---|---|
| 1 | KE205 |
| 2 | KE101 |
| 승객번호 | 편명 |
|---|---|
| 2 | KE205 |
| 승객번호 | 편명 |
|---|---|
| 1 | KE101 |
| 1 | KE205 |
| 2 | KE101 |
| 3 | OZ330 |
MINUS 는 승객번호와 편명을 함께 비교해 탑승에 없는 예약 행만 남기므로 (1, KE205)·(2, KE101) 두 행이 결과이다.
집합 연산자는 중복을 제거하므로 두 번 예약한 (1, KE101)·(3, OZ330) 도 탑승에 한 번 있으면 모두 사라지고, NULL 끼리는 같은 값으로 보아 (4, NULL) 도 제거된다.
- (4, NULL) 이 남은 표는 집합 연산에서 NULL 을 서로 다른 값으로 본 결과이다.
- (2, KE205) 만 있는 표는 탑승에서 예약을 뺀 반대 방향의 결과이다.
- 네 행짜리 표는 중복 건수만큼 빼는 MINUS ALL 처럼 계산한 결과이다.
[예약]
| 출발공항 | 좌석등급 | 운임 |
|---|---|---|
| GMP | ECO | 100 |
| GMP | ECO | 120 |
| GMP | BIZ | 300 |
| ICN | ECO | 200 |
| ICN | BIZ | 500 |
| ICN | ECO | 150 |
SELECT 출발공항, 좌석등급, SUM(운임) AS 합계 FROM 예약 GROUP BY 출발공항, ROLLUP(좌석등급) ORDER BY 출발공항, 좌석등급;
| 출발공항 | 좌석등급 | 합계 |
|---|---|---|
| GMP | BIZ | 300 |
| GMP | ECO | 220 |
| GMP | (null) | 520 |
| ICN | BIZ | 500 |
| ICN | ECO | 350 |
| ICN | (null) | 850 |
| 출발공항 | 좌석등급 | 합계 |
|---|---|---|
| GMP | BIZ | 300 |
| GMP | ECO | 220 |
| ICN | BIZ | 500 |
| ICN | ECO | 350 |
| (null) | BIZ | 800 |
| (null) | ECO | 570 |
| 출발공항 | 좌석등급 | 합계 |
|---|---|---|
| GMP | BIZ | 300 |
| GMP | ECO | 220 |
| GMP | (null) | 520 |
| ICN | BIZ | 500 |
| ICN | ECO | 350 |
| ICN | (null) | 850 |
| (null) | (null) | 1370 |
| 출발공항 | 좌석등급 | 합계 |
|---|---|---|
| GMP | BIZ | 300 |
| GMP | ECO | 220 |
| ICN | BIZ | 500 |
| ICN | ECO | 350 |
| (null) | (null) | 1370 |
출발공항은 ROLLUP 밖에 있어 모든 행의 그룹 기준으로 남으므로, 상세 4행에 출발공항별 소계 2행(520·850)만 더해진 6행이 나오고 총계는 없다.
ROLLUP(좌석등급) 은 (출발공항, 좌석등급) 과 (출발공항) 두 수준만 만든다. 좌석등급이 NULL 인 행이 소계이며 정렬상 각 공항의 마지막에 온다.
- 소계 없이 총계만 있는 표는 ROLLUP 이 총계만 만든다고 본 결과이다.
- 총계 1370 까지 있는 표는 ROLLUP(출발공항, 좌석등급) 의 결과이다.
- 좌석등급별 소계가 붙은 표는 묶는 순서를 거꾸로 본 결과이다.
[예약]
| 항공사 | 출발 | 좌석 |
|---|---|---|
| KE | GMP | ECO |
| KE | GMP | ECO |
| KE | GMP | BIZ |
| KE | ICN | ECO |
| OZ | ICN | ECO |
| OZ | ICN | BIZ |
| OZ | PUS | ECO |
| LJ | GMP | ECO |
SELECT 항공사, 출발, 좌석, COUNT(*) AS 건수 FROM 예약 GROUP BY ROLLUP(항공사, 출발, 좌석);
서로 다른 (항공사, 출발, 좌석) 조합 7개, (항공사, 출발) 소계 5개, 항공사 소계 3개, 총계 1개를 더해 16행이고, 출발이 NULL 인 행은 항공사 소계 3행과 총계 1행으로 4행이다.
KE·GMP·ECO 처럼 같은 조합이 두 번 있어도 상세 그룹은 한 행이며 건수가 2가 된다.
- 17행은 중복 조합을 서로 다른 상세 행으로 센 결과이다.
- 15행·3행은 ROLLUP 이 맨 끝에 만드는 총계 행을 빠뜨린 것이다.
- 출발 NULL 3행은 총계 행에서도 출발이 NULL 이라는 점을 놓친 것이다.
[예약]
| 항공사 | 등급 | 운임 |
|---|---|---|
| KE | BIZ | 500 |
| KE | ECO | 200 |
| OZ | ECO | 180 |
| OZ | ECO | 150 |
| LJ | ECO | 90 |
[SQL 1]
SELECT 항공사, 등급, SUM(운임) AS 합계 FROM 예약 GROUP BY CUBE (항공사, 등급) ORDER BY 항공사, 등급;
[SQL 1 결과]
| 항공사 | 등급 | 합계 |
|---|---|---|
| KE | BIZ | 500 |
| KE | ECO | 200 |
| KE | (null) | 700 |
| LJ | ECO | 90 |
| LJ | (null) | 90 |
| OZ | ECO | 330 |
| OZ | (null) | 330 |
| (null) | BIZ | 500 |
| (null) | ECO | 620 |
| (null) | (null) | 1120 |
[SQL 2]
SELECT 항공사, 등급, SUM(운임) AS 합계 FROM 예약 GROUP BY ( 빈칸 ) ORDER BY 항공사, 등급;
[후보]
㉠ GROUPING SETS ((항공사, 등급), 항공사, ()) ㉡ GROUPING SETS ((항공사, 등급), (등급, 항공사), ()) ㉢ GROUPING SETS (항공사, 등급, ()) ㉣ GROUPING SETS ((항공사, 등급), 항공사, 등급, ())
CUBE(항공사, 등급) 은 두 열로 만들 수 있는 2²=4가지 조합, 곧 (항공사, 등급)·(항공사)·(등급)·() 을 모두 집계하므로 GROUPING SETS 에 네 묶음을 모두 적은 ㉣이 같다.
결과는 상세 4행, 항공사 소계 3행, 등급 소계 2행, 총계 1행으로 10행이다.
- ㉠은 ROLLUP(항공사, 등급) 과 같아 등급별 소계가 없다.
- ㉡의 (등급, 항공사) 는 (항공사, 등급) 과 같은 묶음이라 상세 행이 두 번 나오고 소계가 빠진다.
- ㉢은 소계와 총계만 있고 상세 행이 없다.
[예약]
| 편명 | 운임 |
|---|---|
| KE101 | 100 |
| KE101 | 200 |
| KE101 | 200 |
| KE101 | 500 |
| OZ330 | 400 |
| OZ330 | 600 |
SELECT 편명, 운임,
RATIO_TO_REPORT(운임) OVER (PARTITION BY 편명) AS 비율,
CUME_DIST() OVER (PARTITION BY 편명 ORDER BY 운임) AS 누적
FROM 예약
ORDER BY 편명, 운임;| 편명 | 운임 | 비율 | 누적 |
|---|---|---|---|
| KE101 | 100 | 0.1 | 0.25 |
| KE101 | 200 | 0.2 | 0.5 |
| KE101 | 200 | 0.2 | 0.75 |
| KE101 | 500 | 0.5 | 1 |
| OZ330 | 400 | 0.4 | 0.5 |
| OZ330 | 600 | 0.6 | 1 |
| 편명 | 운임 | 비율 | 누적 |
|---|---|---|---|
| KE101 | 100 | 0.1 | 0.25 |
| KE101 | 200 | 0.2 | 0.5 |
| KE101 | 200 | 0.2 | 0.5 |
| KE101 | 500 | 0.5 | 1 |
| OZ330 | 400 | 0.4 | 0.5 |
| OZ330 | 600 | 0.6 | 1 |
| 편명 | 운임 | 비율 | 누적 |
|---|---|---|---|
| KE101 | 100 | 0.05 | 0.25 |
| KE101 | 200 | 0.1 | 0.75 |
| KE101 | 200 | 0.1 | 0.75 |
| KE101 | 500 | 0.25 | 1 |
| OZ330 | 400 | 0.2 | 0.5 |
| OZ330 | 600 | 0.3 | 1 |
| 편명 | 운임 | 비율 | 누적 |
|---|---|---|---|
| KE101 | 100 | 0.1 | 0.25 |
| KE101 | 200 | 0.2 | 0.75 |
| KE101 | 200 | 0.2 | 0.75 |
| KE101 | 500 | 0.5 | 1 |
| OZ330 | 400 | 0.4 | 0.5 |
| OZ330 | 600 | 0.6 | 1 |
RATIO_TO_REPORT 는 편명별 합계 1000 에 대한 각 운임의 비율이고, CUME_DIST 는 현재 값 이하인 행 수를 파티션 행 수로 나눈 값이다.
KE101 의 200 두 행은 동점이라 100 과 함께 4행 중 3행이 200 이하이므로 둘 다 0.75 이고, OZ330 의 400 은 2행 중 1행이라 0.5 이다.
- 동점 두 행에 0.5 와 0.75 를 나눈 표는 행 번호처럼 차례로 센 결과이다.
- 비율이 절반으로 줄어든 표는 PARTITION 을 무시하고 전체 합 2000 으로 나눈 것이다.
- 동점 행을 0.5 로 둔 표는 순위 2를 행 수 4로 나눈 RANK/N 으로 계산한 것이다.
[예약]
| 예약번호 | 편명 |
|---|---|
| 1 | KE101 |
| 2 | KE101 |
| 3 | LJ007 |
| 4 | KE101 |
| 5 | OZ330 |
| 6 | LJ007 |
| 7 | (null) |
SELECT 편명, COUNT(*) AS 예약수,
COUNT(*) OVER () AS 편수
FROM 예약
GROUP BY 편명
ORDER BY 편명;| 편명 | 예약수 | 편수 |
|---|---|---|
| KE101 | 3 | 4 |
| LJ007 | 2 | 4 |
| OZ330 | 1 | 4 |
| (null) | 1 | 4 |
| 편명 | 예약수 | 편수 |
|---|---|---|
| KE101 | 3 | 7 |
| LJ007 | 2 | 7 |
| OZ330 | 1 | 7 |
| (null) | 1 | 7 |
| 편명 | 예약수 | 편수 |
|---|---|---|
| KE101 | 3 | 3 |
| LJ007 | 2 | 3 |
| OZ330 | 1 | 3 |
| (null) | 1 | 3 |
| 편명 | 예약수 | 편수 |
|---|---|---|
| KE101 | 3 | 3 |
| LJ007 | 2 | 3 |
| OZ330 | 1 | 3 |
논리 처리 순서상 GROUP BY 가 먼저 끝나 편명별 4행(NULL 편명 그룹 포함)이 되고, 윈도우 함수는 이 그룹화된 4행 위에서 COUNT(*) OVER () 를 계산해 모든 행에 4를 준다.
예약수는 각 그룹의 원래 행 수이고, 편명이 NULL 인 7번도 하나의 그룹이 되어 마지막 행으로 나온다.
- 편수를 7로 둔 표는 윈도우 함수가 그룹화 전 원본 행을 센다고 본 것이다.
- 세 행만 있는 표는 GROUP BY 가 NULL 그룹을 버린다고 본 것이다.
- NULL 행은 있지만 편수가 3인 표는 NULL 그룹을 세지 않는다고 오해한 결과이다.
[예약]
| 예약번호 | 편명 | 운임 |
|---|---|---|
| 1 | KE101 | 500 |
| 2 | KE101 | 500 |
| 3 | KE101 | 300 |
| 4 | KE101 | 200 |
| 5 | OZ330 | 400 |
| 6 | OZ330 | 300 |
| 7 | OZ330 | 300 |
SELECT 예약번호, 편명, 운임
FROM (SELECT 예약번호, 편명, 운임,
RANK() OVER (PARTITION BY 편명
ORDER BY 운임 DESC) AS RK
FROM 예약) T
WHERE RK <= 2
ORDER BY 편명, 운임 DESC, 예약번호;| 예약번호 | 편명 | 운임 |
|---|---|---|
| 1 | KE101 | 500 |
| 2 | KE101 | 500 |
| 5 | OZ330 | 400 |
| 6 | OZ330 | 300 |
| 예약번호 | 편명 | 운임 |
|---|---|---|
| 1 | KE101 | 500 |
| 2 | KE101 | 500 |
| 5 | OZ330 | 400 |
| 6 | OZ330 | 300 |
| 7 | OZ330 | 300 |
| 예약번호 | 편명 | 운임 |
|---|---|---|
| 1 | KE101 | 500 |
| 2 | KE101 | 500 |
| 3 | KE101 | 300 |
| 5 | OZ330 | 400 |
| 6 | OZ330 | 300 |
| 7 | OZ330 | 300 |
| 예약번호 | 편명 | 운임 |
|---|---|---|
| 1 | KE101 | 500 |
| 2 | KE101 | 500 |
RANK 는 동점에 같은 순위를 주고 그 수만큼 다음 순위를 건너뛰므로, KE101 은 500 두 건이 1위, 300 이 3위가 되어 예약 1·2만 남는다.
OZ330 은 400 이 1위, 300 두 건이 공동 2위라 예약 5·6·7이 모두 RK ≤ 2 를 만족해 결과는 5행이다.
- 예약 3까지 남은 표는 순위를 건너뛰지 않는 DENSE_RANK 의 결과이다.
- 파티션마다 정확히 2행인 표는 동점에도 고유 번호를 주는 ROW_NUMBER 의 결과이다.
- 예약 1·2만 있는 표는 PARTITION BY 없이 전체에서 순위를 매긴 결과이다.
[운항실적]
| 편명 | 운항일 | 탑승객수 |
|---|---|---|
| KE101 | 2026-03-01 | 120 |
| KE101 | 2026-03-02 | 150 |
| KE101 | 2026-03-03 | 90 |
| KE101 | 2026-03-04 | 200 |
| OZ330 | 2026-03-01 | 80 |
| OZ330 | 2026-03-02 | 100 |
| OZ330 | 2026-03-03 | 60 |
SELECT 편명, 운항일, 탑승객수,
SUM(탑승객수) OVER (PARTITION BY 편명
ORDER BY 운항일
( ㉠ )) AS 합계
FROM 운항실적
ORDER BY 편명, 운항일;[결과]
| 편명 | 운항일 | 탑승객수 | 합계 |
|---|---|---|---|
| KE101 | 2026-03-01 | 120 | 120 |
| KE101 | 2026-03-02 | 150 | 270 |
| KE101 | 2026-03-03 | 90 | 240 |
| KE101 | 2026-03-04 | 200 | 290 |
| OZ330 | 2026-03-01 | 80 | 80 |
| OZ330 | 2026-03-02 | 100 | 180 |
| OZ330 | 2026-03-03 | 60 | 160 |
합계가 KE101 에서 120, 270, 240, 290 으로 직전 1행과 현재 행만 더하고 OZ330 에서 80부터 다시 시작하므로 1 PRECEDING AND CURRENT ROW 이다.
PARTITION BY 로 편명이 바뀌면 프레임이 새로 시작되어, 각 편의 첫 운항일은 앞 행이 없어 자기 값만 합계가 된다.
- UNBOUNDED PRECEDING 이면 처음부터 누적해 KE101 셋째 날이 360 이 된다.
- CURRENT ROW AND 1 FOLLOWING 이면 첫 행이 270 이 되어 결과와 다르다.
- 1 PRECEDING AND 1 FOLLOWING 이면 둘째 날이 360 이 되어 결과와 다르다.
[예약]
| 예약번호 | 운임 |
|---|---|
| 1 | 300 |
| 2 | 520 |
| 3 | 180 |
| 4 | 450 |
| 5 | 610 |
㉠
SELECT * FROM (SELECT ROWNUM AS RN, A.*
FROM (SELECT * FROM 예약 ORDER BY 운임 DESC) A)
WHERE RN BETWEEN 2 AND 3;㉡
SELECT * FROM (SELECT * FROM 예약 ORDER BY 운임 DESC) WHERE ROWNUM BETWEEN 2 AND 3;
㉢
SELECT * FROM 예약 ORDER BY 운임 DESC OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY;
㉣
SELECT 예약번호, 운임
FROM (SELECT 예약번호, 운임,
ROW_NUMBER() OVER (ORDER BY 운임 DESC) AS RN
FROM 예약)
WHERE RN BETWEEN 2 AND 3;㉡은 ROWNUM 을 바로 조건에 썼는데, ROWNUM 은 조건을 통과한 행에만 1부터 붙으므로 첫 행이 ROWNUM 1을 받고 탈락하면 다음 행도 다시 1을 받아 결과가 0행이다.
2~3위처럼 중간 구간을 자르려면 ㉠처럼 ROWNUM 에 별칭을 붙여 한 단계 바깥에서 비교해야 한다.
- ㉠은 정렬된 인라인 뷰에 RN 을 확정한 뒤 걸러 520·450 두 행을 얻는다.
- ㉢은 12c 의 OFFSET·FETCH 로 첫 행을 건너뛰고 두 행을 가져온다.
- ㉣은 ROW_NUMBER 로 순번을 매긴 뒤 바깥에서 순번 2~3을 고른다.
[예약]
| 예약번호 | 등급 | 운임 |
|---|---|---|
| 1 | BIZ | 500 |
| 2 | ECO | 400 |
| 3 | BIZ | 300 |
| 4 | ECO | 300 |
| 5 | BIZ | 300 |
| 6 | ECO | 200 |
SELECT TOP (3) WITH TIES 등급, 운임 FROM 예약 ORDER BY 운임 DESC, 등급;
| 등급 | 운임 |
|---|---|
| BIZ | 500 |
| ECO | 400 |
| BIZ | 300 |
| BIZ | 300 |
| ECO | 300 |
| 등급 | 운임 |
|---|---|
| BIZ | 500 |
| ECO | 400 |
| ECO | 300 |
| 등급 | 운임 |
|---|---|
| BIZ | 500 |
| ECO | 400 |
| BIZ | 300 |
| 등급 | 운임 |
|---|---|
| BIZ | 500 |
| ECO | 400 |
| BIZ | 300 |
| BIZ | 300 |
세 번째 행은 (BIZ, 300) 이고 WITH TIES 는 정렬 키 운임과 등급이 모두 같은 행만 더 가져오므로, 또 다른 (BIZ, 300) 이 붙어 4행이 된다.
(ECO, 300) 은 운임은 같지만 두 번째 키인 등급이 달라 동점이 아니므로 결과에서 빠진다.
- 세 행만 있는 표는 WITH TIES 없이 TOP (3) 만 쓴 결과이다.
- (ECO, 300) 까지 다섯 행인 표는 첫 정렬 키만으로 동점을 판단한 것이다.
- (ECO, 300) 이 셋째인 표는 등급을 내림차순으로 정렬한 것이다.
SELECT A.이름 AS 직원, C.이름 AS 상위자 FROM 승무원 A JOIN 승무원 B ON A.상급자사번 = B.사번 JOIN 승무원 C ON B.상급자사번 = C.사번;
A 의 상급자사번으로 B(직속 상급자)를 찾고, 다시 B 의 상급자사번으로 C 를 찾으므로 C 는 상급자의 상급자, 곧 두 단계 위 사람이다.
같은 테이블을 별칭으로 여러 번 쓰는 셀프 조인은 한 번 조인할 때마다 한 단계씩 위로 올라간다.
- 직속 상급자는 한 번만 조인한 B 의 이름이다.
- 부하를 찾으려면 사번 = 상급자사번 방향을 반대로 걸어야 한다.
- 두 단계 조인은 단계 수가 고정이라 최상위를 보장하지 않는다.
[승무원]
| 사번 | 이름 | 상급자사번 |
|---|---|---|
| 1 | 강기장 | (null) |
| 2 | 남부기장 | 1 |
| 3 | 도사무장 | 1 |
| 4 | 류승무원 | 3 |
| 5 | 문승무원 | 3 |
| 6 | 박승무원 | 2 |
SELECT LEVEL AS LV, 사번, 이름 FROM 승무원 WHERE 사번 <> 3 START WITH 사번 = 1 CONNECT BY PRIOR 사번 = 상급자사번 ORDER BY LV, 사번;
| LV | 사번 | 이름 |
|---|---|---|
| 1 | 1 | 강기장 |
| 2 | 2 | 남부기장 |
| 3 | 4 | 류승무원 |
| 3 | 5 | 문승무원 |
| 3 | 6 | 박승무원 |
| LV | 사번 | 이름 |
|---|---|---|
| 1 | 1 | 강기장 |
| 2 | 2 | 남부기장 |
| 3 | 6 | 박승무원 |
| LV | 사번 | 이름 |
|---|---|---|
| 1 | 1 | 강기장 |
| 2 | 2 | 남부기장 |
| 2 | 4 | 류승무원 |
| 2 | 5 | 문승무원 |
| 3 | 6 | 박승무원 |
| LV | 사번 | 이름 |
|---|---|---|
| 1 | 1 | 강기장 |
| 2 | 2 | 남부기장 |
| 2 | 3 | 도사무장 |
| 3 | 4 | 류승무원 |
| 3 | 5 | 문승무원 |
| 3 | 6 | 박승무원 |
계층형 질의에서 WHERE 절은 전개가 끝난 뒤 적용되므로 도사무장(3) 한 행만 빠지고, 그 아래 류승무원·문승무원은 LEVEL 3 그대로 남아 5행이다.
START WITH 사번 = 1 에서 PRIOR 사번 = 상급자사번 으로 부모에서 자식 방향으로 내려가며, 깊이는 강기장 1, 남부기장·도사무장 2, 나머지 3이다.
- 도사무장 아래가 함께 사라진 표는 조건을 CONNECT BY 절에 둔 결과이다.
- 류승무원·문승무원이 LEVEL 2 인 표는 행을 거른 뒤 깊이를 다시 매긴다고 본 것이다.
- 도사무장이 남은 표는 WHERE 조건을 무시한 결과이다.
[예약]
| 예약번호 | 편명 | 등급 | 운임 |
|---|---|---|---|
| 1 | KE101 | BIZ | 500 |
| 2 | KE101 | ECO | 200 |
| 3 | KE101 | ECO | 150 |
| 4 | OZ330 | ECO | 180 |
| 5 | OZ330 | BIZ | 450 |
SELECT *
FROM 예약
PIVOT (SUM(운임) FOR 등급 IN ('BIZ' AS BIZ, 'ECO' AS ECO))
ORDER BY 예약번호;PIVOT 은 집계(운임)와 FOR(등급)에 쓰지 않은 나머지 열 예약번호·편명을 암시적 GROUP BY 기준으로 삼으므로, 예약번호마다 한 행씩 5행에 예약번호·편명·BIZ·ECO 4열이 된다.
편명별 2행으로 합치려면 FROM 에 편명·등급·운임만 고른 인라인 뷰를 두어야 한다.
- 2행·3열은 인라인 뷰로 예약번호를 뺐을 때의 결과이다.
- 5행·3열은 예약번호 또는 편명 열이 결과에서 사라진다고 본 것이다.
- 2행·4열은 편명으로만 묶이면서 예약번호도 남는다고 본 것이다.
[항공편]
| 편명 |
|---|
| KE101 |
| OZ330 |
| KE7 |
| TWOZ |
| LJ007 |
| ke205 |
| OZ88 |
| KE1234 |
㉠
SELECT COUNT(*) FROM 항공편 WHERE REGEXP_LIKE(편명, '^KE|OZ$');
㉡
SELECT COUNT(*) FROM 항공편
WHERE REGEXP_LIKE(편명, '^(KE|OZ)[0-9]{2,3}$');㉠의 | 는 연산 우선순위가 가장 낮아 ^KE 또는 OZ$ 로 나뉘므로 KE 로 시작하는 KE101·KE7·KE1234 와 OZ 로 끝나는 TWOZ 의 4건이다.
㉡은 괄호로 묶어 KE·OZ 로 시작한 뒤 숫자가 2~3개로 끝나야 하므로 KE101·OZ330·OZ88 의 3건이며, 소문자 ke205 는 대소문자 구분으로 빠진다.
- ㉠을 5건으로 보면 ^ 가 OZ 에도 걸려 OZ330·OZ88 을 세고 TWOZ 를 뺀 셈이다.
- ㉡을 4건으로 보면 숫자 4개인 KE1234 를 {2,3} 에 맞는다고 본 것이다.
- KE7 은 숫자가 1개라 ㉡에 맞지 않고, LJ007 은 두 패턴 모두에 맞지 않는다.
[예약]
| 예약번호 | 승객번호 | 등급 |
|---|---|---|
| 1 | 10 | 일반 |
| 2 | 20 | 일반 |
| 3 | 30 | 일반 |
| 4 | 40 | 일반 |
[승객]
| 승객번호 | 등급 |
|---|---|
| 10 | 골드 |
| 20 | 실버 |
| 30 | 골드 |
UPDATE 예약 R
SET 등급 = (SELECT P.등급
FROM 승객 P
WHERE P.승객번호 = R.승객번호);WHERE 절이 없으므로 예약 4행 모두가 갱신 대상이며, 승객 테이블에 40번이 없는 예약 4는 서브쿼리가 0행이라 NULL 로 갱신된다.
값이 없는 스칼라 서브쿼리는 오류가 아니라 NULL 을 돌려준다. 짝이 있는 행만 바꾸려면 WHERE EXISTS 조건을 함께 써야 한다.
- 짝 없는 행도 대상에 포함되므로 원래 값이 그대로 남지 않는다.
- 서브쿼리 결과가 0행이면 오류 없이 NULL 이 된다.
- UPDATE 는 행을 지우지 않으므로 4행이 그대로 있다.
INSERT INTO 탑승 VALUES (1); COMMIT; INSERT INTO 탑승 VALUES (2); SAVEPOINT SP1; INSERT INTO 탑승 VALUES (3); SAVEPOINT SP1; INSERT INTO 탑승 VALUES (4); ROLLBACK TO SP1; COMMIT; SELECT COUNT(*) FROM 탑승;
같은 이름으로 SAVEPOINT 를 다시 지정하면 나중에 만든 지점이 기준이 되므로, ROLLBACK TO SP1 은 두 번째 SP1 이후의 INSERT 4만 취소한다.
이어지는 COMMIT 으로 2·3이 확정되어 앞서 커밋한 1과 함께 3행이 남는다.
- 1은 ROLLBACK TO 를 마지막 COMMIT 까지 되돌리는 전체 ROLLBACK 으로 본 값이다.
- 2는 첫 번째 SP1 로 돌아가 INSERT 3까지 취소된다고 본 값이다.
- 4는 저장점으로 되돌린 작업이 COMMIT 으로 되살아난다고 본 값이다.
Oracle 의 VARCHAR2 는 가변 길이 문자형이라 VARCHAR2(10) 처럼 최대 길이를 반드시 적어야 하며, 생략하면 오류가 난다.
반면 CHAR·NUMBER·DATE 는 길이나 정밀도를 생략해도 기본값이 적용되어 정상 생성된다.
- CHAR 는 길이를 생략하면 1바이트 고정 길이로 만들어진다.
- NUMBER 는 정밀도를 생략하면 최대 정밀도의 숫자형이 된다.
- DATE 는 길이를 지정하지 않는 날짜형이라 그대로 생성된다.
CREATE TABLE 좌석배정 (
편명 VARCHAR(6),
좌석 VARCHAR(3),
승객번호 INT UNIQUE,
PRIMARY KEY (편명, 좌석)
);
(가) INSERT INTO 좌석배정 VALUES ('KE101', '12A', 1);
(나) INSERT INTO 좌석배정 VALUES ('KE101', '12B', 2);
(다) INSERT INTO 좌석배정 VALUES ('OZ330', '12A', 3);
(라) INSERT INTO 좌석배정 VALUES ('KE101', '12A', 4);
(마) INSERT INTO 좌석배정 VALUES ('KE101', NULL, 5);
(바) INSERT INTO 좌석배정 VALUES ('LJ007', '01C', NULL);복합 PRIMARY KEY 는 (편명, 좌석) 조합이 같은 (라)와 구성 열 좌석이 NULL 인 (마)를 거부하고, 나머지 (가)·(나)·(다)·(바)는 성공해 4개이다.
(다)는 좌석이 같아도 편명이 달라 조합이 다르고, (바)의 승객번호 NULL 은 UNIQUE 가 NULL 을 허용하므로 통과한다.
- 3개는 UNIQUE 열에 NULL 을 넣을 수 없다고 본 값이다.
- 5개는 기본키를 이루는 열 일부는 NULL 이어도 된다고 본 값이다.
- 2개는 좌석만 같아도 기본키 중복이라고 본 값이다.
-- DBA GRANT CREATE TABLE TO KIM WITH ADMIN OPTION; -- KIM GRANT CREATE TABLE TO LEE; -- DBA REVOKE CREATE TABLE FROM KIM;
CREATE TABLE 같은 시스템 권한을 WITH ADMIN OPTION 으로 받아 넘겨준 경우, KIM 의 권한을 회수해도 LEE 에게 준 권한은 연쇄 회수되지 않는다.
객체 권한을 WITH GRANT OPTION 으로 넘긴 경우에는 회수가 연쇄되는 것과 다르다.
- 연쇄 회수는 객체 권한의 WITH GRANT OPTION 에 해당하는 동작이다.
- DBA 가 부여한 권한이므로 DBA 의 REVOKE 는 정상 실행된다.
- 회수 대상은 KIM 이므로 KIM 의 권한이 사라지고 LEE 는 그대로다.