명확화는 누구나 같은 뜻으로 이해하도록 대상을 애매모호함 없이 정확하게 기술하는 특징이다.
모델링의 세 특징은 추상화·단순화·명확화이며, 중복을 피하는 것은 특징이 아니라 모델링 시 유의점에 속한다.
- 일정한 형식에 맞추어 표현하는 것은 추상화다.
- 제한된 표기법과 규약으로 줄여 이해를 돕는 것은 단순화다.
- 같은 데이터를 여러 곳에 두지 않는 것은 중복에 관한 유의점이다.
엔터티는 다른 엔터티와 최소 한 개 이상의 관계를 가져야 하므로, 어떤 업무와도 연결되지 않는 열람석은 이 조건을 충족하지 못한다.
인스턴스가 120건이고 속성도 있지만 관계 조건 하나가 빠져 엔터티로 보기 어렵다.
- 행위 엔터티인 대출도 대출번호라는 식별자를 반드시 가져야 한다.
- 속성은 두 개 이상이면 충분하므로 도서는 조건을 갖췄다.
- 외래키는 성립 조건이 아니며 회원은 대출과 관계를 맺고 있다.
인스턴스는 속성값의 묶음이므로 속성이 없는 엔터티는 인스턴스를 표현할 수 없고, 엔터티는 반드시 속성을 가진다.
관계는 엔터티 하나에 인스턴스 둘 이상, 인스턴스 하나에 속성 둘 이상, 속성 하나에 속성값 하나로 이어진다.
- 인스턴스가 둘 이상이어야 엔터티로 관리할 의미가 있다.
- 한 인스턴스는 여러 속성으로 그 성질을 나타낸다.
- 한 인스턴스의 한 속성에는 값이 하나만 들어간다.
회원 쪽 끝이 바(필수)이고 대출 쪽 끝이 원과 까마귀발이므로, 대출 한 건은 반드시 회원 한 명에게 속하고 회원은 대출이 없을 수도 있다.
IE 표기는 상대 엔터티 쪽 끝의 기호로 차수와 선택성을 읽으며, 원은 0건을 허용한다는 뜻이다.
- 회원카드 쪽 끝이 원이라 카드가 없는 회원도 있을 수 있다.
- 도서와 대출은 1:N 이라 대출 한 건에는 도서가 하나만 연결된다.
- 대출 쪽 끝이 원이라 한 번도 대출되지 않은 도서도 있다.
도서와 대출은 점선인 비식별 관계라 도서번호는 일반 FK 로만 상속되고, 대출의 주식별자는 회원번호와 대출순번 두 속성이다.
유일하지만 대표로 쓰지 않는 이메일은 보조식별자, 다른 엔터티에서 받아 온 회원번호는 외부식별자다.
- 중복 없이 관리되나 주식별자가 아닌 이메일은 보조식별자다.
- 회원번호와 대출순번으로 이루어져 복합식별자다.
- 회원과의 관계로 상속된 회원번호는 외부식별자다.
대출번호 → 회원번호 → 회원명 으로 이어지므로 회원명은 주식별자에 이행 함수 종속되어 이 릴레이션은 3정규형이 아니다.
회원(회원번호, 회원명)을 분리하면 이행 종속이 사라져 3정규형을 만족한다.
- 주식별자가 단일 속성이라 부분 함수 종속은 생길 수 없다.
- 모든 속성이 원자값이므로 1정규형은 만족한다.
- 이행 종속이 남아 있어 3정규형이 아니다.
관계를 맺으면 부모의 주식별자가 자식에 외래키(FK)로 상속되므로, 대출.회원번호와 회원.회원번호를 같다고 연결하면 된다.
모델의 관계선이 곧 조인 경로이며 PK와 FK를 = 로 연결하는 것이 조인 조건이다.
- 대출번호와 회원번호는 의미가 다른 값이라 조인 조건이 될 수 없다.
- 조건 없이 곱하면 카티션 곱이 되어 엉뚱한 조합이 생긴다.
- FK 가 있으므로 열을 중복 추가하지 않아도 조인으로 조회된다.
함께 반영되어야 하는 두 변경은 하나의 트랜잭션으로 묶어 모두 성공하면 COMMIT, 하나라도 실패하면 ROLLBACK 한다.
이것이 트랜잭션의 원자성으로, 반납일만 기록되고 도서는 대출중으로 남는 불일치를 막는다.
- 반납일을 먼저 커밋하면 상태 변경이 실패했을 때 되돌릴 수 없다.
- 성공한 쪽만 유지하면 두 테이블의 상태가 어긋난다.
- 별도 트랜잭션으로 나누면 함께 반영된다는 보장이 사라진다.
[업무 규칙]
· 회원번호와 회원명은 가입 시 반드시 입력한다.
· 휴대폰은 회원이 원할 때만 입력하는 선택 항목이다.
· 가입일은 가입 시 시스템이 자동으로 기록한다.
· 탈퇴일은 탈퇴한 회원에게만 기록한다.
[회원]
| 회원번호 | 회원명 | 휴대폰 | 가입일 | 탈퇴일 |
|---|---|---|---|---|
| M01 | 한지수 | 010-2345-1001 | 2026-01-05 | (null) |
| M02 | 오세린 | 010-2345-1002 | 2026-01-12 | 2026-04-30 |
| M03 | 남궁현 | 010-2345-1003 | 2026-02-03 | (null) |
| M04 | 배유나 | 010-2345-1004 | 2026-02-20 | (null) |
NULL 허용 여부는 현재 데이터가 아니라 업무 규칙으로 정하므로, 선택 입력인 휴대폰과 탈퇴한 회원에게만 생기는 탈퇴일이 NULL 허용 대상이다.
지금 휴대폰 값이 모두 채워져 있어도 입력하지 않는 회원이 생길 수 있으므로 NOT NULL 로 두면 가입이 막힌다.
- 현재 데이터에서 NULL 이 보이는 탈퇴일만 고르면 휴대폰이 빠진다.
- 가입일은 시스템이 항상 기록하므로 NOT NULL 이 맞다.
- 회원명은 가입 시 필수 입력이라 NOT NULL 이다.
세 속성으로 된 본질식별자를 그대로 쓰면 연체기록 같은 자식마다 세 열을 FK 로 상속해야 하므로, 인조식별자는 상속 키와 조인 조건을 단순하게 만든다.
대신 업무 중복을 막으려면 본질식별자 조합에 별도의 유니크 제약을 두어야 한다.
- 순번만으로는 같은 대출이 두 번 입력되는 것을 막지 못한다.
- 업무 의미가 드러나는 것은 본질식별자의 장점이다.
- 본질식별자 중복 방지를 위해 오히려 인덱스가 추가로 필요하다.
관계형 데이터베이스에서 행(튜플)의 개수를 카디널리티라 하므로 이 테이블의 카디널리티는 120이다.
열(속성)의 개수는 차수라 하며 이 테이블의 차수는 5다.
- 차수는 열의 개수라 120이 아니라 5다.
- 튜플은 행이므로 튜플 수는 120이다.
- 속성은 열이므로 속성 수는 5다.
[대출]
| 대출번호 | 회원번호 | 도서번호 | 반납일 |
|---|---|---|---|
| 1 | M01 | B1 | 2026-03-05 |
| 2 | M01 | B2 | (null) |
| 3 | M02 | B3 | 2026-03-07 |
| 4 | M02 | B1 | 2026-03-10 |
| 5 | M03 | B4 | (null) |
| 6 | M03 | B2 | (null) |
| 7 | M01 | B5 | 2026-03-12 |
| 8 | M02 | B6 | 2026-03-15 |
SELECT 회원번호, COUNT(*) AS 반납수 FROM 대출 WHERE 반납일 IS NOT NULL GROUP BY 회원번호 ORDER BY 반납수 DESC, 회원번호;
| 회원번호 | 반납수 |
|---|---|
| M01 | 2 |
| M02 | 3 |
| 회원번호 | 반납수 |
|---|---|
| M02 | 3 |
| M01 | 2 |
| 회원번호 | 반납수 |
|---|---|
| M02 | 3 |
| M01 | 2 |
| M03 | 0 |
| 회원번호 | 반납수 |
|---|---|
| M01 | 3 |
| M02 | 3 |
| M03 | 2 |
논리적 처리 순서는 FROM → WHERE → GROUP BY → SELECT → ORDER BY 이므로, 반납일이 있는 행만 먼저 남긴 뒤 회원별로 세어 M02 3건, M01 2건이 된다.
반납 행이 하나도 없는 M03 은 WHERE 에서 모두 걸러져 그룹 자체가 생기지 않고, ORDER BY 는 SELECT 다음이라 별칭 반납수로 내림차순 정렬된다.
- M01 3건·M03 2건이 보이는 표는 WHERE 를 적용하지 않은 결과다.
- M01 이 먼저 나온 표는 반납수를 오름차순으로 정렬했다.
- M03 0건 행은 걸러진 그룹이 0건으로 남는다고 오해한 것이다.
NULL 은 알 수 없는 값이라 어떤 수를 더하거나 빼거나 곱해도 NULL이 되므로 연체료 + 0 은 NULL 이다.
NULL 과의 비교는 TRUE·FALSE 가 아닌 UNKNOWN이며, NULL 여부는 IS NULL 로 판별한다.
- 0 을 곱해도 NULL 이 전파되어 결과는 NULL 이다.
- 자기 자신을 빼도 값을 모르므로 결과는 NULL 이다.
- = NULL 비교는 UNKNOWN 이라 TRUE 가 되지 않는다.
[도서]
| 도서번호 | 청구기호 | 쪽수 |
|---|---|---|
| B1 | 813.6-한12 | 250 |
| B2 | 911-이7 | 300 |
| B3 | 005.13-박104 | 401 |
| B4 | 400 | 120 |
SELECT 도서번호,
SUBSTR(청구기호, 1, INSTR(청구기호, '-') - 1) AS 분류,
SUBSTR(청구기호, -2) AS 끝,
CEIL(쪽수 / 100) AS 묶음
FROM 도서
ORDER BY 도서번호;| 도서번호 | 분류 | 끝 | 묶음 |
|---|---|---|---|
| B1 | 813.6 | 12 | 3 |
| B2 | 911 | 이7 | 3 |
| B3 | 005.13 | 04 | 5 |
| B4 | (null) | 00 | 2 |
| 도서번호 | 분류 | 끝 | 묶음 |
|---|---|---|---|
| B1 | 813.6 | 12 | 3 |
| B2 | 911 | 이7 | 3 |
| B3 | 005.13 | 04 | 4 |
| B4 | (null) | 00 | 1 |
| 도서번호 | 분류 | 끝 | 묶음 |
|---|---|---|---|
| B1 | 813.6 | 12 | 2 |
| B2 | 911 | 이7 | 3 |
| B3 | 005.13 | 04 | 4 |
| B4 | (null) | 00 | 1 |
| 도서번호 | 분류 | 끝 | 묶음 |
|---|---|---|---|
| B1 | 813.6 | 12 | 3 |
| B2 | 911 | 이7 | 3 |
| B3 | 005.13 | 04 | 5 |
| B4 | 400 | 00 | 2 |
B4 는 하이픈이 없어 INSTR 결과가 0 이고 SUBSTR 길이가 -1 이 되므로 Oracle 은 NULL을 돌려주며, SUBSTR(청구기호, -2) 는 뒤에서 두 글자를 잘라 00 이 된다.
CEIL은 소수를 올리므로 2.5·3·4.01·1.2 가 차례로 3·3·5·2 가 된다.
- B4 분류를 400 으로 본 표는 INSTR 가 0 일 때 문자열 전체가 나온다고 오해했다.
- 4.01 을 4, 1.2 를 1 로 둔 표는 CEIL 을 반올림처럼 계산했다.
- 2.5 를 2 로 둔 표는 CEIL 을 FLOOR 처럼 내림으로 계산했다.
[회원]
| 회원번호 | 이름 | 연체일 |
|---|---|---|
| M1 | 윤채원 | 3 |
| M2 | 석도하 | (null) |
| M3 | 편예린 | 0 |
| M4 | 진우현 | 10 |
| M5 | 고아라 | (null) |
SELECT 회원번호, ( ㉠ ) AS 연체료 FROM 회원 ORDER BY 회원번호;
[원하는 결과]
| 회원번호 | 연체료 |
|---|---|
| M1 | 300 |
| M2 | 0 |
| M3 | 0 |
| M4 | 1000 |
| M5 | 0 |
COALESCE(연체일, 0) 이 NULL 을 0 으로 바꾼 뒤 100 을 곱하므로 M2·M5 는 0, 나머지는 300·0·1000 이 되어 원하는 결과와 같다.
NULL 에 곱셈을 먼저 하면 NULL 이 전파되므로 NULL 치환은 연산 전에 해야 한다.
- NULLIF 는 0 인 M3 까지 NULL 로 만들어 연체료가 비게 된다.
- 대체값이 NULL 인 COALESCE 는 NULL 행을 그대로 NULL 로 둔다.
- ELSE 없는 CASE 는 연체일이 있는 행을 NULL 로 만든다.
[대출]
| 대출번호 | 대출일 | 반납일 |
|---|---|---|
| L1 | 2026-03-01 | 2026-03-15 |
| L2 | 2026-03-01 | 2026-03-20 |
| L3 | 2026-02-20 | 2026-03-10 |
| L4 | 2026-03-10 | (null) |
| L5 | 2026-03-05 | 2026-03-18 |
SELECT 대출번호, 반납일 - (대출일 + 14) AS 연체일수 FROM 대출 WHERE 반납일 > 대출일 + 14 ORDER BY 대출번호;
| 대출번호 | 연체일수 |
|---|---|
| L2 | 5 |
| L3 | 4 |
| 대출번호 | 연체일수 |
|---|---|
| L2 | 5 |
| L3 | 6 |
| 대출번호 | 연체일수 |
|---|---|
| L2 | 5 |
| L3 | 4 |
| L4 | (null) |
| 대출번호 | 연체일수 |
|---|---|
| L1 | 0 |
| L2 | 5 |
| L3 | 4 |
DATE 에 14 를 더하면 14일 뒤 날짜가 되고 두 DATE 를 빼면 일수가 나오므로, 기한을 넘긴 L2 는 5일, 2월 20일 대출로 기한이 3월 6일인 L3 은 4일이다.
L1 은 기한 당일 반납이라 > 조건을 통과하지 못하고, 반납일이 NULL 인 L4 는 비교가 UNKNOWN이라 빠진다.
- L1 을 0일로 넣은 표는 > 를 >= 처럼 해석했다.
- L3 을 6일로 본 표는 2026년 2월을 30일로 계산했다.
- L4 를 NULL 로 넣은 표는 NULL 비교 행이 남는다고 오해했다.
[도서]
| 도서번호 | 제목 | 분류 |
|---|---|---|
| B1 | DB_설계 | 기술 |
| B2 | DB설계 | 기술 |
| B3 | 봄_여름 | (null) |
| B4 | 겨울밤 | (null) |
| B5 | 여름밤 | 소설 |
| B6 | 가을 | 역사 |
SELECT 도서번호, 제목
FROM 도서
WHERE 제목 LIKE '%#_%' ESCAPE '#'
OR 분류 IN ('소설', NULL)
ORDER BY 도서번호;| 도서번호 | 제목 |
|---|---|
| B1 | DB_설계 |
| B5 | 여름밤 |
| 도서번호 | 제목 |
|---|---|
| B1 | DB_설계 |
| B3 | 봄_여름 |
| B4 | 겨울밤 |
| B5 | 여름밤 |
| 도서번호 | 제목 |
|---|---|
| B1 | DB_설계 |
| B2 | DB설계 |
| B3 | 봄_여름 |
| B4 | 겨울밤 |
| B5 | 여름밤 |
| B6 | 가을 |
| 도서번호 | 제목 |
|---|---|
| B1 | DB_설계 |
| B3 | 봄_여름 |
| B5 | 여름밤 |
ESCAPE '#' 로 #_ 가 밑줄 문자 자체를 뜻하므로 제목에 _ 가 든 B1·B3 이 일치하고, IN 목록에서 소설과 같은 B5 가 더해진다.
IN 의 NULL 은 = NULL 비교라 UNKNOWN이 되어 분류가 NULL 인 B4 를 잡지 못하며, B3 은 LIKE 가 TRUE 라 OR 로 통과한다.
- B4 를 넣은 표는 IN 목록의 NULL 이 NULL 행과 일치한다고 오해했다.
- 여섯 행이 모두 나온 표는 _ 를 한 글자 와일드카드로 해석했다.
- B3 을 뺀 표는 분류가 NULL 이면 OR 전체가 거짓이 된다고 본 것이다.
NULL 과의 비교는 UNKNOWN이므로 등급 <> 'VIP' 는 일반 5행만 남기고 NULL 2행은 제외한다.
등급 = NULL 은 모든 행에서 UNKNOWN 이라 0행이며, NULL 행을 찾으려면 IS NULL을 써야 한다.
- 부정 조건에 NULL 행이 포함된다고 보면 7행이 되지만 실제로는 빠진다.
- = NULL 로 NULL 2행이 조회된다고 보는 것은 잘못이다.
- 두 오해를 합친 7행·2행도 실제 결과와 다르다.
[대출]
| 대출번호 | 지점 | 회원번호 | 연체료 |
|---|---|---|---|
| 1 | 본관 | M1 | 500 |
| 2 | 본관 | M2 | (null) |
| 3 | 본관 | M1 | 400 |
| 4 | 분관 | M3 | (null) |
| 5 | 분관 | M3 | (null) |
| 6 | 분관 | M4 | 600 |
SELECT 지점,
COUNT(DISTINCT 회원번호) AS 회원수,
COUNT(연체료) AS 건수,
AVG(연체료) AS 평균,
SUM(연체료) / COUNT(*) AS 환산
FROM 대출
GROUP BY 지점
ORDER BY 지점;| 지점 | 회원수 | 건수 | 평균 | 환산 |
|---|---|---|---|---|
| 본관 | 2 | 2 | 300 | 300 |
| 분관 | 2 | 1 | 200 | 200 |
| 지점 | 회원수 | 건수 | 평균 | 환산 |
|---|---|---|---|---|
| 본관 | 2 | 3 | 450 | 300 |
| 분관 | 2 | 3 | 600 | 200 |
| 지점 | 회원수 | 건수 | 평균 | 환산 |
|---|---|---|---|---|
| 본관 | 2 | 2 | 450 | 300 |
| 분관 | 2 | 1 | 600 | 200 |
| 지점 | 회원수 | 건수 | 평균 | 환산 |
|---|---|---|---|---|
| 본관 | 3 | 2 | 450 | 300 |
| 분관 | 3 | 1 | 600 | 200 |
본관은 회원 M1·M2 로 회원수 2, 연체료가 있는 2건의 평균 450, 합계 900을 전체 3행으로 나눈 환산 300 이고, 분관은 회원수 2, 건수 1, 평균 600, 환산 200 이다.
AVG는 NULL 을 분모에서 빼지만 SUM(연체료)/COUNT(*) 는 NULL 행도 분모에 넣어 값이 달라진다.
- 회원수 3은 COUNT(DISTINCT) 의 중복 제거를 무시한 값이다.
- 평균 300·200 은 NULL 을 0 으로 보고 평균을 낸 값이다.
- 건수 3은 COUNT(열) 이 NULL 까지 센다고 오해한 값이다.
GROUP BY 가 없으면 테이블 전체가 하나의 그룹이 되어 결과가 1행이므로, 행마다 값이 다른 회원번호를 집계 없이 함께 SELECT 하면 오류가 난다.
SELECT 절에는 집계 함수, 상수, 집계 함수끼리의 연산만 둘 수 있다.
- 집계 함수만 나열한 SQL 은 전체 1행으로 정상 실행된다.
- 문자 상수는 그룹마다 같은 값이라 집계와 함께 써도 된다.
- MAX 와 MIN 의 차처럼 집계 결과끼리의 연산은 허용된다.
[대출]
| 대출번호 | 회원번호 | 대출일 | 연체일 |
|---|---|---|---|
| 1 | M1 | 2026-02-25 | 7 |
| 2 | M1 | 2026-03-02 | 0 |
| 3 | M1 | 2026-03-09 | 2 |
| 4 | M2 | 2026-03-03 | 6 |
| 5 | M3 | 2026-03-04 | (null) |
| 6 | M3 | 2026-02-27 | 3 |
| 7 | M4 | 2026-03-05 | 1 |
| 8 | M4 | 2026-03-12 | (null) |
| 9 | M5 | 2026-02-10 | 9 |
SELECT 회원번호, COUNT(*) AS 건수, SUM(연체일) AS 합계 FROM 대출 WHERE 대출일 >= DATE '2026-03-01' GROUP BY 회원번호 HAVING COUNT(*) >= 2 OR SUM(연체일) > 5 ORDER BY 회원번호;
| 회원번호 | 건수 | 합계 |
|---|---|---|
| M1 | 2 | 2 |
| M2 | 1 | 6 |
| M3 | 1 | (null) |
| M4 | 2 | 1 |
| 회원번호 | 건수 | 합계 |
|---|---|---|
| M1 | 2 | 2 |
| M2 | 1 | 6 |
| M4 | 2 | 1 |
| 회원번호 | 건수 | 합계 |
|---|---|---|
| M1 | 2 | 2 |
| M2 | 1 | 6 |
| M4 | 2 | (null) |
| 회원번호 | 건수 | 합계 |
|---|---|---|
| M1 | 3 | 9 |
| M2 | 1 | 6 |
| M4 | 2 | 1 |
WHERE가 먼저 3월 대출만 남기므로 M1 은 2건 합계 2, M2 는 1건 6, M3 은 1건 NULL, M4 는 2건 1 이 되고 M5 는 그룹이 생기지 않는다.
HAVING에서 M1·M4 는 2건 이상, M2 는 합계가 5 초과라 남고, M3 은 1건이며 NULL > 5 가 UNKNOWN 이라 빠진다.
- M1 이 3건·9 인 표는 2월 대출까지 집계해 WHERE 를 무시했다.
- M3 을 넣은 표는 NULL 비교가 조건을 만족한다고 오해했다.
- M4 합계를 NULL 로 둔 표는 SUM 이 NULL 을 하나라도 만나면 NULL 이 된다고 봤다.
[도서]
| 도서번호 | 분류 | 대출횟수 |
|---|---|---|
| B1 | 소설 | 5 |
| B2 | 기술 | 3 |
| B3 | 소설 | 8 |
| B4 | 기술 | 6 |
| B5 | 역사 | 4 |
| B6 | 기술 | 6 |
SELECT 도서번호, 대출횟수 * 2 AS 점수 FROM 도서 ORDER BY 분류, 점수 DESC, 1 DESC;
| 도서번호 | 점수 |
|---|---|
| B2 | 6 |
| B6 | 12 |
| B4 | 12 |
| B1 | 10 |
| B3 | 16 |
| B5 | 8 |
| 도서번호 | 점수 |
|---|---|
| B6 | 12 |
| B4 | 12 |
| B2 | 6 |
| B3 | 16 |
| B1 | 10 |
| B5 | 8 |
| 도서번호 | 점수 |
|---|---|
| B4 | 12 |
| B6 | 12 |
| B2 | 6 |
| B3 | 16 |
| B1 | 10 |
| B5 | 8 |
| 도서번호 | 점수 |
|---|---|
| B3 | 16 |
| B6 | 12 |
| B4 | 12 |
| B1 | 10 |
| B5 | 8 |
| B2 | 6 |
SELECT 목록에 없는 분류로 먼저 기술·소설·역사 순으로 묶고, 같은 분류 안에서는 별칭 점수 내림차순, 점수가 같은 B4·B6 은 열 번호 1(도서번호) 내림차순이라 B6 이 앞선다.
ORDER BY 에는 선택하지 않은 열, 별칭, 열 번호를 섞어 쓸 수 있고 방향은 키마다 따로 적용된다.
- 점수만으로 전체를 정렬한 표는 선택하지 않은 분류 키를 무시했다.
- B4 를 B6 앞에 둔 표는 마지막 키의 DESC 를 무시했다.
- 분류 안에서 점수가 커지는 표는 점수를 오름차순으로 정렬했다.
[연체기준]
| 등급 | 최소일 | 최대일 |
|---|---|---|
| 경고 | 1 | 6 |
| 정지 | 7 | 29 |
| 제명 | 30 | (null) |
[대출]
| 대출번호 | 연체일수 |
|---|---|
| L1 | 0 |
| L2 | 3 |
| L3 | 7 |
| L4 | 15 |
| L5 | 30 |
| L6 | 45 |
| L7 | (null) |
SELECT L.대출번호, G.등급
FROM 대출 L
JOIN 연체기준 G
ON L.연체일수 >= G.최소일
AND L.연체일수 <= G.최대일
ORDER BY L.대출번호;범위 조건으로 결합하는 NON-EQUI 조인에서 L2 는 경고, L3·L4 는 정지와 맞아 결과는 3행이다.
제명의 최대일이 NULL 이라 30·45 와의 <= 비교가 UNKNOWN이 되어 L5·L6 이 빠지고, 연체일수가 0 인 L1 과 NULL 인 L7 도 맞는 등급이 없다.
- 5행은 NULL 상한을 상한 없음으로 보아 L5·L6 까지 넣은 값이다.
- 4행은 경계값 30 인 L5 만 최소일과 같아 결합된다고 본 값이다.
- 7행은 INNER 조인이 대출 행을 모두 남긴다고 오해한 값이다.
SELECT M.회원번호, L.대출번호 FROM 회원 M, 대출 L WHERE M.등급 = 'VIP';
FROM 절에 두 테이블을 두고 조인 조건을 주지 않았으므로 VIP 회원 2행 각각이 대출 5행 전부와 결합되어 10행이 된다.
WHERE 의 등급 조건은 회원 쪽 행만 줄일 뿐이고, 남은 행끼리는 카티션 곱으로 짝지어진다.
- 2행은 조인 없이 VIP 회원만 센 값이다.
- 7행은 두 쪽 행 수를 더한 값이다.
- 20행은 등급 조건을 무시하고 전체 4행과 곱한 값이다.
[회원]
| 회원번호 | 회원명 |
|---|---|
| 1 | 추하람 |
| 2 | 모가온 |
| 3 | 탁세윤 |
| 4 | 제나리 |
[대출]
| 대출번호 | 회원번호 |
|---|---|
| L1 | 2 |
| L2 | 2 |
| L3 | 3 |
| L4 | 5 |
| L5 | (null) |
회원번호 2 가 대출 2건, 3 이 1건과 맞아 INNER JOIN은 3행이고, LEFT 는 짝이 없는 회원 1·4 가 더해져 5행이다.
FULL OUTER JOIN은 여기에 대출 쪽 미매칭인 회원번호 5 와 NULL 행까지 더해 7행이 된다.
- FULL 이 6행이면 회원번호가 NULL 인 대출 행을 빠뜨린 것이다.
- LEFT 가 4행이면 미매칭 회원 1·4 중 하나만 센 것이다.
- INNER 가 2행이면 회원번호 2 의 대출 2건을 1건으로 센 것이다.
[회원]
| 회원번호 | 이름 |
|---|---|
| 1 | 김하늘 |
| 2 | 이도현 |
| 3 | 박서윤 |
| 4 | 최민준 |
| 5 | 정유나 |
[대출]
| 대출번호 | 회원번호 | 도서번호 | 상태 |
|---|---|---|---|
| 101 | 1 | 11 | 반납 |
| 102 | 1 | 12 | 대출중 |
| 103 | 2 | 13 | 반납 |
| 104 | 3 | 11 | 대출중 |
| 105 | 3 | 14 | 대출중 |
| 106 | 5 | 12 | 반납 |
| 107 | 5 | 15 | 반납 |
SELECT M.회원번호, M.이름
FROM 회원 M LEFT OUTER JOIN 대출 L
ON M.회원번호 = L.회원번호
( ㉠ )
ORDER BY M.회원번호;[결과]
| 회원번호 | 이름 |
|---|---|
| 2 | 이도현 |
| 4 | 최민준 |
| 5 | 정유나 |
회원별 '대출중' 행만 ON 절에서 결합한 뒤, 결합 상대가 없어 NULL 로 채워진 회원만 WHERE 에서 고르는 ON 절 조건 + WHERE IS NULL 조합이 정답이다.
ON 조건은 결합 여부만 정하고 왼쪽 행은 보존하므로 이도현(반납만)·최민준(이력 없음)·정유나(반납만)가 NULL 결합 행으로 남아 3행이 된다.
WHERE L.상태 = '반납' OR L.상태 IS NULL은 대출 중 도서가 있는 김하늘도 반납 행 때문에 남고 정유나는 두 번 나온다.WHERE L.대출번호 IS NULL은 대출 이력이 전혀 없는 최민준만 남긴다.AND L.상태 IN ('대출중', '반납')은 걸러 내는 조건이 없어 모든 회원이 대출 건수만큼 나온다.
[도서]
| 도서번호 | 제목 | 분류코드 |
|---|---|---|
| 1 | 코스모스 | SC |
| 2 | 총균쇠 | HI |
| 3 | 데미안 | (null) |
| 4 | 이기적 유전자 | SC |
[분류]
| 분류코드 | 분류명 |
|---|---|
| SC | 과학 |
| HI | 역사 |
| LI | 문학 |
SELECT * FROM 도서 NATURAL JOIN 분류 ORDER BY 도서번호;
| 분류코드 | 도서번호 | 제목 | 분류명 |
|---|---|---|---|
| SC | 1 | 코스모스 | 과학 |
| HI | 2 | 총균쇠 | 역사 |
| (null) | 3 | 데미안 | (null) |
| SC | 4 | 이기적 유전자 | 과학 |
| 분류코드 | 도서번호 | 제목 | 분류명 |
|---|---|---|---|
| SC | 1 | 코스모스 | 과학 |
| HI | 2 | 총균쇠 | 역사 |
| SC | 4 | 이기적 유전자 | 과학 |
| 도서번호 | 제목 | 분류코드 | 분류명 |
|---|---|---|---|
| 1 | 코스모스 | SC | 과학 |
| 2 | 총균쇠 | HI | 역사 |
| 4 | 이기적 유전자 | SC | 과학 |
| 분류코드 | 도서번호 | 제목 | 분류코드 | 분류명 |
|---|---|---|---|---|
| SC | 1 | 코스모스 | SC | 과학 |
| HI | 2 | 총균쇠 | HI | 역사 |
| SC | 4 | 이기적 유전자 | SC | 과학 |
NATURAL JOIN 은 이름이 같은 분류코드로 등가 조인하고, SELECT * 에서 공통 열을 맨 앞에 한 번만 출력한다.
분류코드가 NULL 인 데미안과 도서가 없는 문학(LI)은 짝이 없어 빠지므로 결과는 3행이다.
- 공통 열을 도서 테이블 순서대로 가운데에 둔 표는 열 배치가 틀렸다.
- 데미안 행을 남긴 표는 OUTER JOIN 처럼 처리한 결과이다.
- 분류코드가 두 번 나오는 표는 WHERE 등가 조인에서 SELECT * 를 쓴 결과이다.
[도서]
| 도서번호 | 분류 | 가격 |
|---|---|---|
| 1 | 과학 | 18000 |
| 2 | 과학 | 25000 |
| 3 | 과학 | (null) |
| 4 | 문학 | 15000 |
| 5 | 문학 | 22000 |
| 6 | 역사 | 30000 |
| 7 | 역사 | 18000 |
SELECT 도서번호, 분류, 가격
FROM 도서
WHERE 분류 IN ('문학', '역사')
AND 가격 ( ㉠ ) (SELECT 가격
FROM 도서
WHERE 분류 = '과학')
ORDER BY 도서번호;[결과]
| 도서번호 | 분류 | 가격 |
|---|---|---|
| 5 | 문학 | 22000 |
| 6 | 역사 | 30000 |
결과는 과학 도서 가격 중 어느 하나보다만 커도 참이 되는 > ANY 조건, 즉 최솟값 18000 을 넘는 행이다.
ANY 는 서브쿼리의 NULL 을 무시하고 하나라도 참이면 통과하므로 22000·30000 은 남고 18000 은 같아서 빠진다.
> ALL은 서브쿼리에 NULL 이 있어 모든 비교가 참이 될 수 없으므로 0행이다.IN은 값이 같은 행만 고르므로 가격이 18000 인 도서번호 7 만 나온다.NOT IN도 서브쿼리 결과에 NULL 이 있어 모든 비교가 UNKNOWN 이 되므로 0행이다.
연관 서브쿼리는 서브쿼리 안에서 메인쿼리의 열을 참조하므로, 논리적으로 메인쿼리 행마다 그 값을 받아 다시 평가된다.
예를 들어 회원별 평균 대출 권수보다 많이 빌린 대출 건을 찾을 때 바깥 행의 회원번호를 서브쿼리 조건에 넘긴다.
- 한 번만 실행해 결과를 재사용하는 것은 메인쿼리를 참조하지 않는 비연관 서브쿼리의 특징이다.
- 연관 서브쿼리는 WHERE·SELECT 절 등에 두루 쓰이며 인라인 뷰로 한정되지 않는다.
- EXISTS 는 연관 서브쿼리와 가장 흔히 함께 쓰이는 연산자이다.
[회원]
| 회원번호 | 이름 |
|---|---|
| 1 | 한서진 |
| 2 | 오태윤 |
| 3 | 문지아 |
| 4 | 배현우 |
[대출]
| 대출번호 | 회원번호 | 대출일 |
|---|---|---|
| 1 | 1 | 2026-03-02 |
| 2 | 1 | 2026-03-15 |
| 3 | 3 | 2026-03-09 |
| 4 | 1 | 2026-02-20 |
| 5 | 3 | 2026-03-21 |
SELECT M.이름,
(SELECT COUNT(*) FROM 대출 L
WHERE L.회원번호 = M.회원번호) AS 대출수,
(SELECT MAX(L.대출일) FROM 대출 L
WHERE L.회원번호 = M.회원번호) AS 최근대출
FROM 회원 M
ORDER BY M.회원번호;| 이름 | 대출수 | 최근대출 |
|---|---|---|
| 한서진 | 3 | 2026-03-15 |
| 문지아 | 2 | 2026-03-21 |
| 이름 | 대출수 | 최근대출 |
|---|---|---|
| 한서진 | 3 | 2026-03-15 |
| 오태윤 | (null) | (null) |
| 문지아 | 2 | 2026-03-21 |
| 배현우 | (null) | (null) |
| 이름 | 대출수 | 최근대출 |
|---|---|---|
| 한서진 | 3 | 2026-03-15 |
| 오태윤 | 0 | (null) |
| 문지아 | 2 | 2026-03-21 |
| 배현우 | 0 | (null) |
| 이름 | 대출수 | 최근대출 |
|---|---|---|
| 한서진 | 3 | 2026-03-15 |
| 오태윤 | 1 | (null) |
| 문지아 | 2 | 2026-03-21 |
| 배현우 | 1 | (null) |
스칼라 서브쿼리는 메인쿼리 행마다 한 값을 돌려주므로 회원 4명이 모두 남고, 대출이 없으면 COUNT 는 0, MAX 는 NULL 이 된다.
한서진은 대출 3건 중 가장 늦은 3월 15일, 문지아는 2건 중 3월 21일이 최근대출이다.
- 대출 없는 회원이 빠진 표는 INNER JOIN 처럼 처리한 결과이다.
- 대출수를 NULL 로 둔 표는 대상 행이 없을 때 COUNT 도 NULL 이라고 본 경우이다.
- 대출수를 1로 둔 표는 LEFT OUTER JOIN 후 COUNT(*) 로 NULL 결합 행까지 센 결과이다.
[대출]
| 회원번호 | 대출월 |
|---|---|
| 1 | 2 |
| 1 | 3 |
| 2 | 2 |
| 2 | 2 |
| 3 | 3 |
| 4 | 2 |
| 5 | 3 |
[연체]
| 회원번호 |
|---|
| 2 |
| 5 |
| 6 |
SELECT 회원번호 FROM 대출 WHERE 대출월 = 2 UNION SELECT 회원번호 FROM 대출 WHERE 대출월 = 3 MINUS SELECT 회원번호 FROM 연체 ORDER BY 1;
| 회원번호 |
|---|
| 1 |
| 2 |
| 3 |
| 4 |
| 회원번호 |
|---|
| 1 |
| 3 |
| 4 |
| 6 |
| 회원번호 |
|---|
| 1 |
| 1 |
| 3 |
| 4 |
| 회원번호 |
|---|
| 1 |
| 3 |
| 4 |
Oracle 의 집합 연산자는 우선순위가 같아 위에서부터 차례로 처리되므로, 2월·3월 회원의 UNION {1,2,3,4,5} 에서 연체 회원 {2,5,6} 을 MINUS 한 {1,3,4} 가 결과이다.
UNION 단계에서 2월에 두 번 빌린 회원번호 2 와 두 달 모두 빌린 회원번호 1 은 한 행으로 합쳐지고, 연체에만 있는 6 은 차집합에 영향을 주지 않는다.
- {1,2,3,4} 는 MINUS 를 먼저 계산해 3월 쪽에서만 연체 회원을 뺀 결과이다.
- 회원번호 1 이 두 번 나온 표는 UNION 이 중복을 남긴다고 본 경우이다.
- 6 이 포함된 표는 MINUS 를 양쪽 차이를 모두 남기는 연산으로 오해한 결과이다.
집합 연산에서 ORDER BY 는 마지막 SELECT 뒤에 한 번만 써서 전체 결과를 정렬하며, 개별 SELECT 에 두면 오류가 난다.
집합 연산의 조건은 각 SELECT 의 열 개수 일치와 대응 열의 타입 호환이며, 열 이름은 같을 필요가 없다.
- 열 개수가 다르면 행을 위아래로 이어 붙일 수 없어 오류이다.
- 숫자 열과 문자 열처럼 타입이 호환되지 않으면 오류가 난다.
- 열 이름이 달라도 실행되며 결과 열 이름은 첫 SELECT 를 따른다.
[대출통계]
| 지점 | 분류 | 권수 |
|---|---|---|
| 본관 | 문학 | 3 |
| 본관 | 과학 | 2 |
| 본관 | 문학 | 4 |
| 분관 | 과학 | 5 |
| 분관 | 문학 | 1 |
SELECT 지점, 분류, SUM(권수) AS 합계 FROM 대출통계 GROUP BY 지점, ROLLUP(분류) ORDER BY 지점, 분류;
| 지점 | 분류 | 합계 |
|---|---|---|
| 본관 | 과학 | 2 |
| 본관 | 문학 | 7 |
| 분관 | 과학 | 5 |
| 분관 | 문학 | 1 |
| (null) | 과학 | 7 |
| (null) | 문학 | 8 |
| 지점 | 분류 | 합계 |
|---|---|---|
| 본관 | 과학 | 2 |
| 본관 | 문학 | 7 |
| 본관 | (null) | 9 |
| 분관 | 과학 | 5 |
| 분관 | 문학 | 1 |
| 분관 | (null) | 6 |
| (null) | (null) | 15 |
| 지점 | 분류 | 합계 |
|---|---|---|
| 본관 | 과학 | 2 |
| 본관 | 문학 | 7 |
| 본관 | (null) | 9 |
| 분관 | 과학 | 5 |
| 분관 | 문학 | 1 |
| 분관 | (null) | 6 |
| 지점 | 분류 | 합계 |
|---|---|---|
| 본관 | (null) | 9 |
| 본관 | 과학 | 2 |
| 본관 | 문학 | 7 |
| 분관 | (null) | 6 |
| 분관 | 과학 | 5 |
| 분관 | 문학 | 1 |
GROUP BY 지점, ROLLUP(분류) 는 (지점, 분류) 그룹과 지점별 소계만 만들고, ROLLUP 밖의 지점은 항상 묶이므로 총계 행은 없다.
결과는 상세 4행과 소계 2행으로 6행이며, 소계 행의 분류는 NULL 이라 오름차순 정렬에서 각 지점의 마지막에 온다.
- 총계 15 행을 붙인 표는 ROLLUP(지점, 분류) 와 혼동한 결과이다.
- 소계가 지점마다 먼저 나온 표는 NULL 을 가장 작은 값으로 정렬한 경우이다.
- 분류별 합계가 붙은 표는 ROLLUP 안의 열을 기준으로 소계를 낸다고 본 것이다.
[대출현황]
| 등급 | 매체 | 건수 |
|---|---|---|
| 일반 | 도서 | 4 |
| 일반 | 영상 | 1 |
| 우수 | 도서 | 3 |
| 우수 | 영상 | 2 |
| 일반 | 도서 | 2 |
SELECT 등급, 매체, SUM(건수) AS 합계 FROM 대출현황 GROUP BY ( ㉠ ) ORDER BY 1, 2;
[결과]
| 등급 | 매체 | 합계 |
|---|---|---|
| 우수 | 도서 | 3 |
| 우수 | 영상 | 2 |
| 우수 | (null) | 5 |
| 일반 | 도서 | 6 |
| 일반 | 영상 | 1 |
| 일반 | (null) | 7 |
| (null) | 도서 | 9 |
| (null) | 영상 | 3 |
| (null) | (null) | 12 |
결과에 등급별 소계, 매체별 소계, 총계가 모두 있으므로 두 열의 모든 조합 2²=4가지 그룹을 만드는 CUBE(등급, 매체) 가 정답이다.
상세 4행 + 등급 소계 2행 + 매체 소계 2행 + 총계 1행으로 9행이 되며, 일반·도서처럼 원본이 두 건인 조합은 한 행으로 합쳐진다.
- ROLLUP(등급, 매체) 는 등급이 NULL 인 매체별 소계 행을 만들지 않는다.
- GROUPING SETS(등급, 매체) 는 소계만 만들어 상세 행과 총계가 없다.
- 일반 GROUP BY 는 등급·매체 조합별 상세 4행만 나온다.
[대출집계]
| 지점 | 분류 | 권수 |
|---|---|---|
| 본관 | 문학 | 3 |
| 본관 | (null) | 2 |
| 본관 | 과학 | 4 |
| 분관 | 문학 | 5 |
| 분관 | 문학 | 1 |
| 분관 | (null) | 2 |
SELECT 지점,
CASE WHEN GROUPING(분류) = 1 THEN '소계'
ELSE COALESCE(분류, '미분류') END AS 구분,
SUM(권수) AS 합계
FROM 대출집계
GROUP BY GROUPING SETS ((지점, 분류), (지점), ())
ORDER BY 1, 2;GROUPING(분류) 는 집계 과정에서 분류가 묶여 생긴 NULL 에만 1을 주므로, 지점 소계 2행과 총계 1행이 '소계'가 되어 3행이다.
원래 분류가 NULL 인 데이터는 (지점, 분류) 그룹에서 GROUPING 값이 0이라 COALESCE 로 '미분류'가 되며, 본관·분관에 하나씩 2행이다.
- 2와 2는 총계 행도 분류가 집계된 행이라는 점을 놓친 경우이다.
- 5와 0은 원래 NULL 과 집계로 생긴 NULL 을 구분하지 못한 경우이다.
- 3과 1은 지점이 다른 NULL 분류를 한 그룹으로 합친 경우이다.
[대출]
| 대출번호 | 분류 |
|---|---|
| 1 | 문학 |
| 2 | 과학 |
| 3 | 문학 |
| 4 | 역사 |
| 5 | 문학 |
| 6 | 과학 |
SELECT 분류,
COUNT(*) AS 건수,
SUM(COUNT(*)) OVER () AS 전체,
RANK() OVER (ORDER BY COUNT(*) DESC) AS 순위
FROM 대출
GROUP BY 분류
ORDER BY 분류;| 분류 | 건수 | 전체 | 순위 |
|---|---|---|---|
| 과학 | 2 | 2 | 2 |
| 문학 | 3 | 3 | 1 |
| 역사 | 1 | 1 | 3 |
| 분류 | 건수 | 전체 | 순위 |
|---|---|---|---|
| 과학 | 2 | 3 | 2 |
| 문학 | 3 | 3 | 1 |
| 역사 | 1 | 3 | 3 |
| 분류 | 건수 | 전체 | 순위 |
|---|---|---|---|
| 과학 | 2 | 6 | 2 |
| 문학 | 3 | 6 | 1 |
| 역사 | 1 | 6 | 3 |
| 분류 | 건수 | 전체 | 순위 |
|---|---|---|---|
| 과학 | 2 | 6 | 2 |
| 과학 | 2 | 6 | 2 |
| 문학 | 3 | 6 | 1 |
| 문학 | 3 | 6 | 1 |
| 문학 | 3 | 6 | 1 |
| 역사 | 1 | 6 | 3 |
윈도우 함수는 GROUP BY 집계가 끝난 뒤 그룹 결과 3행 위에서 계산되므로 행 수는 분류 수인 3행이고, SUM(COUNT(*)) OVER () 는 세 그룹 건수의 합 6이다.
순위는 그룹 건수 내림차순이라 문학 3건이 1위, 과학 2건이 2위, 역사 1건이 3위가 된다.
- 전체 열이 각 그룹 건수와 같은 표는 OVER () 가 그룹마다 나뉜다고 본 결과이다.
- 전체가 3인 표는 그룹 수를 센 COUNT(*) OVER () 의 결과이다.
- 6행이 나온 표는 윈도우 함수가 원본 행을 되살린다고 본 것이다.
[도서]
| 도서번호 | 제목 | 분류 | 대출수 |
|---|---|---|---|
| 1 | 파친코 | 문학 | 12 |
| 2 | 데미안 | 문학 | 12 |
| 3 | 토지 | 문학 | 8 |
| 4 | 어린왕자 | 문학 | (null) |
| 5 | 코스모스 | 과학 | 7 |
| 6 | 시간의 역사 | 과학 | 7 |
SELECT 분류, 제목,
RANK() OVER (PARTITION BY 분류
ORDER BY 대출수 DESC) AS R,
DENSE_RANK() OVER (PARTITION BY 분류
ORDER BY 대출수 DESC) AS D
FROM 도서
ORDER BY 분류, R, 도서번호;| 분류 | 제목 | R | D |
|---|---|---|---|
| 과학 | 코스모스 | 1 | 1 |
| 과학 | 시간의 역사 | 2 | 2 |
| 문학 | 어린왕자 | 1 | 1 |
| 문학 | 파친코 | 2 | 2 |
| 문학 | 데미안 | 2 | 2 |
| 문학 | 토지 | 4 | 3 |
| 분류 | 제목 | R | D |
|---|---|---|---|
| 과학 | 코스모스 | 1 | 1 |
| 과학 | 시간의 역사 | 1 | 1 |
| 문학 | 파친코 | 1 | 1 |
| 문학 | 데미안 | 1 | 1 |
| 문학 | 토지 | 3 | 2 |
| 문학 | 어린왕자 | 4 | 3 |
| 분류 | 제목 | R | D |
|---|---|---|---|
| 과학 | 코스모스 | 1 | 1 |
| 과학 | 시간의 역사 | 1 | 1 |
| 문학 | 어린왕자 | 1 | 1 |
| 문학 | 파친코 | 2 | 2 |
| 문학 | 데미안 | 2 | 2 |
| 문학 | 토지 | 3 | 3 |
| 분류 | 제목 | R | D |
|---|---|---|---|
| 과학 | 코스모스 | 1 | 1 |
| 과학 | 시간의 역사 | 1 | 1 |
| 문학 | 어린왕자 | 1 | 1 |
| 문학 | 파친코 | 2 | 2 |
| 문학 | 데미안 | 2 | 2 |
| 문학 | 토지 | 4 | 3 |
Oracle 은 내림차순 정렬에서 NULL 을 가장 앞에 두므로 문학에서 어린왕자가 1위가 되고, 12 동점 둘이 2위, 토지는 RANK 4·DENSE_RANK 3 이다.
순위는 PARTITION BY 분류로 분류마다 1부터 다시 매겨지며, 과학의 7 동점 두 권은 두 함수 모두 1이다.
- 어린왕자를 맨 뒤 4위로 둔 표는 NULL 을 가장 작은 값으로 본 SQL Server 식 정렬이다.
- 토지의 RANK 를 3으로 둔 표는 RANK 가 동점 뒤 순위를 건너뛰지 않는다고 본 것이다.
- 시간의 역사를 2위로 둔 표는 동점에도 고유 번호를 주는 ROW_NUMBER 와 혼동한 결과이다.
[대출]
| 대출번호 | 회원번호 | 대출일 | 연체료 |
|---|---|---|---|
| 1 | 10 | 2026-03-02 | 300 |
| 2 | 20 | 2026-03-03 | 500 |
| 3 | 10 | 2026-03-05 | 200 |
| 4 | 20 | 2026-03-08 | 0 |
| 5 | 10 | 2026-03-09 | 400 |
| 6 | 30 | 2026-03-10 | 100 |
SELECT 회원번호, 대출일, 연체료,
SUM(연체료) OVER ( ㉠ ) AS 누적
FROM 대출
ORDER BY 회원번호, 대출일;[결과]
| 회원번호 | 대출일 | 연체료 | 누적 |
|---|---|---|---|
| 10 | 2026-03-02 | 300 | 300 |
| 10 | 2026-03-05 | 200 | 500 |
| 10 | 2026-03-09 | 400 | 900 |
| 20 | 2026-03-03 | 500 | 500 |
| 20 | 2026-03-08 | 0 | 500 |
| 30 | 2026-03-10 | 100 | 100 |
결과의 누적 값이 회원이 바뀔 때마다 새로 시작하고 대출일 순으로 쌓이므로 PARTITION BY 회원번호 ORDER BY 대출일 이 정답이다.
ORDER BY 가 있으면 기본 프레임이 파티션 처음부터 현재 행까지라 회원번호 10 은 300·500·900, 연체료 0 이 있는 20 은 500·500 이 된다.
ORDER BY 대출일만 쓰면 회원 구분 없이 전체 일자 순으로 누적된다.PARTITION BY 회원번호만 쓰면 회원별 총합 하나가 모든 행에 반복된다.PARTITION BY 대출일은 대출일이 모두 달라 각 행의 연체료가 그대로 나온다.
[일별대출]
| 대출일 | 건수 |
|---|---|
| 2026-03-01 | 4 |
| 2026-03-02 | 6 |
| 2026-03-03 | 2 |
| 2026-03-04 | 5 |
| 2026-03-05 | 3 |
SELECT 대출일,
SUM(건수) OVER (ORDER BY 대출일
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS 이동합,
LAST_VALUE(건수) OVER (ORDER BY 대출일
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS 마지막
FROM 일별대출
ORDER BY 대출일;| 대출일 | 이동합 | 마지막 |
|---|---|---|
| 2026-03-01 | 10 | 4 |
| 2026-03-02 | 12 | 6 |
| 2026-03-03 | 13 | 2 |
| 2026-03-04 | 10 | 5 |
| 2026-03-05 | 8 | 3 |
| 대출일 | 이동합 | 마지막 |
|---|---|---|
| 2026-03-01 | 4 | 3 |
| 2026-03-02 | 10 | 3 |
| 2026-03-03 | 8 | 3 |
| 2026-03-04 | 7 | 3 |
| 2026-03-05 | 8 | 3 |
| 대출일 | 이동합 | 마지막 |
|---|---|---|
| 2026-03-01 | (null) | 3 |
| 2026-03-02 | 12 | 3 |
| 2026-03-03 | 13 | 3 |
| 2026-03-04 | 10 | 3 |
| 2026-03-05 | (null) | 3 |
| 대출일 | 이동합 | 마지막 |
|---|---|---|
| 2026-03-01 | 10 | 3 |
| 2026-03-02 | 12 | 3 |
| 2026-03-03 | 13 | 3 |
| 2026-03-04 | 10 | 3 |
| 2026-03-05 | 8 | 3 |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING 은 앞·현재·뒤 행 최대 3행을 합하며, 첫 행과 마지막 행은 있는 2행만 더해 10과 8이 된다.
LAST_VALUE 의 프레임을 UNBOUNDED FOLLOWING 까지 넓혔으므로 모든 행에서 마지막 날 건수 3이 나온다.
- 양 끝을 NULL 로 둔 표는 프레임 행이 모자라면 NULL 이 된다고 본 경우이다.
- 마지막 열이 건수와 같은 표는 기본 프레임처럼 현재 행에서 끝난다고 본 결과이다.
- 이동합이 4·10·8 로 시작하는 표는 1 FOLLOWING 을 빼고 앞 행과 현재 행만 더한 결과이다.
[회원]
| 회원번호 | 이름 | 대출수 |
|---|---|---|
| 1 | 유재희 | 15 |
| 2 | 김보람 | (null) |
| 3 | 안시우 | 12 |
| 4 | 홍다인 | 15 |
| 5 | 고은찬 | 12 |
| 6 | 류세아 | 9 |
| 7 | 백지후 | 12 |
SELECT TOP (3) WITH TIES 회원번호, 대출수 FROM 회원 ORDER BY 대출수 DESC;
SQL Server 는 NULL 을 가장 작은 값으로 보아 내림차순에서 맨 뒤에 두고, WITH TIES 는 3번째 행과 대출수가 같은 행을 모두 포함하므로 15·15·12·12·12 의 5행, 합계 66이다.
세 번째 행의 값 12 를 가진 회원이 셋이라 TOP (3) 이어도 동점 행이 함께 나온다.
- 3행·30 은 Oracle 처럼 NULL 이 내림차순 맨 앞에 온다고 본 경우이다.
- 3행·42 는 WITH TIES 를 무시하고 정확히 3행만 자른 경우이다.
- 6행·75 는 서로 다른 상위 값 3개(15·12·9)를 모두 가져온다고 본 경우이다.
㉠
SELECT 제목
FROM (SELECT 제목,
ROW_NUMBER() OVER (ORDER BY 대출수 DESC) AS RN
FROM 도서)
WHERE RN <= 3;㉡
SELECT 제목,
ROW_NUMBER() OVER (ORDER BY 대출수 DESC) AS RN
FROM 도서
WHERE RN <= 3;㉢
SELECT 제목 FROM 도서 ORDER BY 대출수 DESC FETCH FIRST 3 ROWS ONLY;
㉣
SELECT 제목 FROM (SELECT 제목 FROM 도서 ORDER BY 대출수 DESC) WHERE ROWNUM <= 3;
㉡은 같은 SELECT 절에서 만든 별칭 RN 을 WHERE 절에서 쓰는데, WHERE 는 SELECT 보다 먼저 처리되고 윈도우 함수는 WHERE 절에 쓸 수 없으므로 오류가 난다.
ROW_NUMBER 로 Top N 을 구하려면 ㉠처럼 인라인 뷰에서 순번을 만든 뒤 바깥 쿼리에서 걸러야 한다.
- ㉠은 인라인 뷰에서 만든 RN 을 바깥 WHERE 에서 비교하므로 정상 실행된다.
- ㉢의 FETCH FIRST 3 ROWS ONLY 는 Oracle 12c 부터 지원하는 Top N 구문이다.
- ㉣은 정렬된 인라인 뷰에 ROWNUM 조건을 걸어 상위 3행을 바르게 구한다.
[분류]
| 분류코드 | 분류명 | 상위분류코드 |
|---|---|---|
| 1 | 전체 | (null) |
| 2 | 인문 | 1 |
| 3 | 과학 | 1 |
| 4 | 철학 | 2 |
| 5 | 역사 | 2 |
| 6 | 한국사 | 5 |
| 7 | 물리 | 3 |
| 8 | 서양사 | 5 |
SELECT LEVEL, 분류명 FROM 분류 START WITH 분류명 = '인문' CONNECT BY PRIOR 분류코드 = 상위분류코드;
START WITH 로 지정한 인문이 LEVEL 1 이 되고, PRIOR 분류코드 = 상위분류코드 는 부모의 코드를 자식의 상위코드로 찾는 순방향 전개라 인문의 하위만 펼쳐진다.
인문(1)·철학(2)·역사(2)·한국사(3)·서양사(3)의 5행이며, 인문의 상위인 전체와 다른 가지인 과학·물리는 나오지 않는다.
- 5와 4는 LEVEL 을 시작 행이 아닌 최상위 전체부터 센 경우이다.
- 2와 2는 PRIOR 위치를 반대로 읽어 인문에서 전체로 올라간 역방향 결과이다.
- 4와 2는 시작 행이 결과에 포함되지 않는다고 본 경우이다.
[분류]
| 분류코드 | 분류명 | 상위코드 |
|---|---|---|
| 1 | 문학 | (null) |
| 2 | 소설 | 1 |
| 3 | 시 | 1 |
| 4 | 한국소설 | 2 |
| 5 | 외국소설 | 2 |
SELECT 분류명,
CONNECT_BY_ROOT 분류명 AS 루트,
CONNECT_BY_ISLEAF AS 잎,
SYS_CONNECT_BY_PATH(분류명, '/') AS 경로
FROM 분류
START WITH 상위코드 IS NULL
CONNECT BY PRIOR 분류코드 = 상위코드;한국소설은 하위 분류가 없는 말단이므로 CONNECT_BY_ISLEAF 가 1이다.
CONNECT_BY_ROOT 는 모든 행에 시작 행인 문학을 돌려주고, SYS_CONNECT_BY_PATH 는 구분자로 시작해 /문학/소설/한국소설처럼 현재 행에서 끝난다.
- 소설 행의 루트 값은 자기 자신이 아니라 최상위 분류인 문학이다.
- 문학 행의 경로는 구분자가 앞에 붙은 '/문학'이다.
- 경로는 구분자로 시작하고 현재 행의 이름으로 끝나므로 '/' 로 끝나지 않는다.
[회원대출]
| 회원 | 도서 | DVD | 전자책 |
|---|---|---|---|
| 한결 | 3 | (null) | 1 |
| 서아 | (null) | 2 | (null) |
| 도윤 | (null) | (null) | (null) |
| 나래 | 4 | 1 | 2 |
㉠
SELECT 회원, 매체, 건수 FROM 회원대출 UNPIVOT (건수 FOR 매체 IN (도서, DVD, 전자책));
㉡
SELECT 회원, 매체, 건수
FROM 회원대출
UNPIVOT INCLUDE NULLS
(건수 FOR 매체 IN (도서, DVD, 전자책));UNPIVOT 은 기본값이 EXCLUDE NULLS 라 값이 있는 칸만 행이 되므로 ㉠은 한결 2·서아 1·도윤 0·나래 3의 6행이다.
INCLUDE NULLS 를 주면 NULL 칸도 행으로 남아 회원 4명×매체 3개=12행이 되며, 모든 값이 NULL 인 도윤도 3행으로 나온다.
- 6과 9는 INCLUDE NULLS 에서도 값이 전부 NULL 인 원본 행은 사라진다고 본 경우이다.
- 7과 12는 기본 동작에서 전부 NULL 인 도윤이 한 행으로 남는다고 본 경우이다.
- 12와 12는 기본값이 NULL 칸도 행으로 만든다고 본 경우이다.
[도서]
| 도서번호 | 청구기호 |
|---|---|
| 6 | D903.1-2025-07 |
| 7 | K813.7-2026-12 |
| 8 | S400-2024-03 |
SELECT REGEXP_REPLACE(청구기호, '[^0-9]') AS R FROM 도서 WHERE 도서번호 = 7;
대괄호 안의 ^ 는 부정이라 [^0-9] 는 숫자가 아닌 한 글자를 뜻하고, Oracle 의 REGEXP_REPLACE 는 바꿀 문자열을 생략하면 일치한 부분을 지우며 기본으로 모든 일치를 처리한다.
K·점·하이픈 두 개가 모두 지워져 숫자만 이어 붙은 8137202612 가 된다.
- 813.7-2026-12 는 첫 번째 일치인 K 만 지운다고 본 결과이다.
- K.-- 는 ^ 를 무시해 숫자를 지운다고 본 결과이다.
- 813 은 첫 숫자 덩어리만 뽑는 REGEXP_SUBSTR 과 혼동한 결과이다.
· 테이블 구조는 그대로 남는다.
· WHERE 절로 삭제할 행을 고를 수 있다.
· COMMIT 전이면 ROLLBACK 으로 되돌릴 수 있다.
WHERE 로 일부 행만 지울 수 있고 커밋 전 롤백이 가능한 명령은 DML 인 DELETE 이며, 행만 지우므로 구조는 남는다.
DELETE 는 행마다 로그를 남겨 되돌릴 수 있는 대신, 전체 삭제라면 TRUNCATE 에 비해 느리다.
- TRUNCATE 는 구조는 남기지만 WHERE 를 쓸 수 없고 Oracle 에서 자동 커밋돼 롤백할 수 없다.
- DROP 은 데이터와 함께 테이블 구조까지 제거한다.
- ALTER 는 테이블 구조를 바꾸는 DDL 로 행 삭제 명령이 아니다.
INSERT INTO 회원 VALUES (1, '강도윤'); BEGIN TRANSACTION; INSERT INTO 회원 VALUES (2, '송하은'); INSERT INTO 회원 VALUES (3, '임재원'); ROLLBACK; INSERT INTO 회원 VALUES (4, '윤아린');
SQL Server 기본값인 AUTO COMMIT 모드에서는 명시적 트랜잭션 밖의 문장이 실행 즉시 커밋되므로 강도윤과 윤아린 2행이 남는다.
BEGIN TRANSACTION 으로 연 명시적 트랜잭션 안의 두 INSERT 만 ROLLBACK 으로 함께 취소된다.
- 0은 COMMIT 을 하기 전까지 모든 변경이 미확정이라고 본 경우이다.
- 1은 ROLLBACK 이 앞서 자동 커밋된 첫 INSERT 까지 취소한다고 본 경우이다.
- 4는 트랜잭션 안의 INSERT 도 각각 자동 커밋되어 ROLLBACK 이 효과가 없다고 본 경우이다.
CREATE TABLE 회원 (
회원번호 INT PRIMARY KEY,
이름 VARCHAR(20) NOT NULL,
등급 VARCHAR(10) CHECK (등급 IN ('일반', '우수'))
);
CREATE TABLE 대출 (
대출번호 INT PRIMARY KEY,
회원번호 INT REFERENCES 회원(회원번호),
반납일 DATE
);
INSERT INTO 회원 VALUES (1, '윤채원', '일반');㉠ INSERT INTO 대출 VALUES (10, NULL, NULL); ㉡ INSERT INTO 대출 VALUES (11, 2, NULL); ㉢ INSERT INTO 회원 VALUES (2, '서지호', 'VIP'); ㉣ INSERT INTO 대출 VALUES (12, 1, DATE '2026-03-10');
외래키 열의 NULL 은 참조 검사를 받지 않아 ㉠은 입력되고, 부모에 있는 회원번호 1 을 참조한 ㉣도 입력된다.
㉡은 회원 테이블에 없는 회원번호 2 를 참조해 참조 무결성 위반이고, ㉢은 CHECK 목록에 없는 'VIP' 라 거부된다.
- ㉣만 고른 것은 외래키 열에 NULL 을 넣을 수 없다고 본 경우이다.
- ㉡을 포함한 것은 부모 행이 없어도 FK 값을 넣을 수 있다고 본 경우이다.
- ㉢을 포함한 것은 CHECK 제약이 목록 밖 입력값을 막지 않는다고 본 경우이다.
CREATE TABLE 회원 ( 회원번호 NUMBER PRIMARY KEY, 이름 VARCHAR2(20), 등급 VARCHAR2(10), 이메일 VARCHAR2(40) );
Oracle 에서 열의 타입·크기·NULL 여부를 바꾸는 구문은 MODIFY 이며, ALTER COLUMN 은 SQL Server 구문이라 Oracle 에서는 오류가 난다.
Oracle 은 ADD·MODIFY 에 괄호로 열 정의를 묶을 수 있고, 열 삭제는 DROP COLUMN 으로 한다.
- ADD (열 정의) 는 새 열을 추가하는 Oracle 의 올바른 구문이다.
- MODIFY 로 크기 변경과 NOT NULL 지정을 함께 할 수 있으며 데이터가 없으므로 성공한다.
- DROP COLUMN 은 지정한 열과 그 데이터를 삭제하는 올바른 구문이다.
( ㉠ ) 사서역할; GRANT SELECT, INSERT, UPDATE ON 대출 TO 사서역할; GRANT 사서역할 TO 김사서, 박사서;
권한을 묶어 이름을 붙이는 객체는 ROLE 이며, CREATE ROLE 로 만든 뒤 GRANT 로 권한을 담고 다시 사용자에게 부여한다.
사서가 늘거나 권한이 바뀌어도 ROLE 하나만 고치면 되므로 권한 관리가 단순해진다.
- CREATE USER 는 계정을 만드는 명령이라 권한 묶음을 만들어 나눠 줄 수 없다.
- GRANT ROLE 이라는 생성 구문은 없고 ROLE 부여는 GRANT 역할명 TO 사용자 형식이다.
- CREATE SCHEMA 는 여러 객체를 한 번에 만드는 구문으로 권한 묶음과 무관하다.