데이터베이스 정규화와 SQL 판단법
함수 종속으로 정규형을 판별하고 SQL 실행 결과를 빠르게 추적하는 방법을 다룹니다.
정규화 순서
제1정규형은 각 속성에 원자값을 저장합니다. 제2정규형은 비주요 속성이 모든 후보키에 완전 함수 종속되어야 하고, 제3정규형은 비자명한 함수 종속 X → A마다 X가 슈퍼키이거나 A가 주요 속성이어야 합니다. BCNF는 비자명한 함수 종속의 결정자가 항상 슈퍼키여야 합니다. 제4정규형은 비자명한 다치 종속의 결정자가 슈퍼키인지, 제5정규형은 비자명한 조인 종속이 후보키에서 유도되는지 확인합니다.
- 정규화 순서는 1NF → 2NF → 3NF → BCNF → 4NF → 5NF로 이어진다.
- 슈퍼키는 행을 유일하게 식별하는 속성 집합이고, 후보키는 불필요한 속성이 없는 최소 슈퍼키다. 주요 속성은 하나 이상의 후보키에 포함된 속성이다.
- 기본키만 보지 말고 모든 후보키를 확인한다. 비주요 속성이 복합 후보키의 일부에만 종속되면 제2정규형을 위반한다.
- 제4정규형은 다치 종속, 제5정규형은 조인 종속이 핵심 판단어다.
- 무손실 분해는 분해한 릴레이션을 조인했을 때 원래 릴레이션이 정확히 복원되는 성질이다.
- 종속성 보존은 원래 함수 종속을 조인 없이 분해된 릴레이션만으로 검사할 수 있는지를 본다.
- 정규화는 이상 현상을 줄이지만 조인 비용이 늘 수 있고, 반정규화는 성능을 위한 의도적 중복이다.
JOIN과 NULL
INNER JOIN은 양쪽에 일치하는 행만, OUTER JOIN은 기준 방향의 불일치 행도 보존합니다. NULL은 값이 없거나 알려지지 않은 상태이며 숫자 0과 다릅니다. NULL 여부는 IS NULL로 검사합니다. 빈 문자열 처리는 DBMS에 따라 다르며 Oracle SQL은 길이가 0인 문자열을 NULL로 취급합니다.
- WHERE 조건이 OUTER JOIN 결과를 다시 제거할 수 있다.
- COUNT(*)는 행을, COUNT(열)은 NULL을 제외한 값을 센다.
- COALESCE는 첫 번째 NULL이 아닌 값을 반환한다.
GROUP BY와 윈도우 함수
GROUP BY는 여러 행을 그룹 단위로 축약하지만 윈도우 함수는 원래 행을 유지한 채 순위·누적값을 계산합니다. WHERE는 그룹화 전, HAVING은 그룹화 후 조건입니다.
- RANK는 동률 다음 순위를 건너뛴다.
- ROW_NUMBER는 동률에도 고유 번호를 준다.
- 집계함수와 일반 열을 함께 조회할 때 그룹 기준을 확인한다.
종속성과 SQL 실행 순서를 실제로 추적하기
정규화 문제는 후보키를 먼저 찾고 비주요 속성이 키 전체에 종속되는지, 다른 비주요 속성을 거쳐 종속되는지를 차례로 검사합니다. SQL 결과 문제는 FROM·JOIN, WHERE, GROUP BY, HAVING, SELECT, ORDER BY의 논리적 처리 순서에 맞춰 중간 결과를 작게 그리면 NULL과 집계 함정을 피할 수 있습니다.
- 부분 함수 종속은 복합 후보키의 일부에 종속될 때만 성립한다.
- OUTER JOIN 뒤 WHERE에서 반대편 열을 제한하면 보존했던 NULL 행이 제거될 수 있다.
- 서브쿼리가 여러 행을 반환할 수 있으면 = 대신 IN·ANY·ALL의 의미를 비교한다.
집합 연산·서브쿼리·인덱스 판단
UNION·INTERSECT·EXCEPT는 대응 열의 개수와 자료형 호환성을 먼저 확인하고, 중복 보존 여부는 ALL 유무로 구분합니다. 인덱스는 검색을 빠르게 하지만 갱신 비용과 저장 공간을 요구하므로 선택도와 사용 조건을 함께 판단해야 합니다.
- UNION은 기본적으로 중복을 제거하고 UNION ALL은 중복을 보존한다.
- EXISTS는 서브쿼리 결과 행의 존재 여부를 판단하며 SELECT 목록의 실제 값은 중요하지 않다.
- NOT IN의 비교 집합에 NULL이 있으면 UNKNOWN 때문에 예상과 다른 결과가 날 수 있다.
- B-트리 계열 인덱스는 범위 검색에 유리하지만 열에 함수를 적용하면 일반 인덱스를 활용하지 못할 수 있다.
데이터 모델링·키·관계대수 핵심 구분
개념 모델은 업무 관점의 엔터티와 관계, 논리 모델은 속성·식별자·정규화, 물리 모델은 자료형·인덱스·파티션처럼 구현 세부사항을 다룹니다. 관계대수는 결과도 릴레이션인 폐쇄성을 가지며 선택·투영·조인·나눗셈의 피연산자와 결과를 구분해야 합니다.
- 엔터티는 업무에서 관리할 필요가 있는 식별 가능한 대상이고 인스턴스는 그 구체적인 한 건이다.
- 후보키는 유일성과 최소성을 모두 만족하며, 기본키로 선택되지 않은 후보키는 대체키가 된다.
- 식별 관계는 부모 기본키가 자식 기본키에 포함되고 비식별 관계는 일반 외래키로 이어지는 구조다.
- 선택은 행, 투영은 열을 고르며 자연 조인은 공통 속성의 동등 조건과 중복 열 제거를 포함한다.
- 나눗셈 R(학생, 과목)÷S(과목)은 S의 모든 과목과 연결된 학생만 반환한다. R={(가, DB),(가, OS),(나, DB)}, S={(DB),(OS)}이면 결과는 {(가)}다. 한 과목이라도 이수한 학생을 찾는 연산과 다르다.
SQLD 함수·계층형·윈도우·관리 구문
SQLD 결과 문제는 단일행 함수와 그룹 함수를 먼저 구분하고, 윈도우 함수는 행을 축약하지 않는다는 점을 기준으로 풉니다. 계층형 질의·Top N·PIVOT은 시험에서 사용하는 Oracle SQL 표현을 전제로 할 수 있으므로 지문의 방언과 처리 순서를 확인합니다.
- CASE와 조건 함수는 NULL 비교 방식과 최초로 참이 되는 분기를 확인하고 문자·날짜 함수는 반환 자료형을 함께 본다.
- RANK는 동률 다음 순위를 건너뛰고 DENSE_RANK는 건너뛰지 않으며 ROW_NUMBER는 동률에도 서로 다른 번호를 준다.
- 윈도우 프레임 ROWS는 물리적 행 수, RANGE는 정렬값의 동등 범위를 기준으로 포함 행이 달라질 수 있다.
- DML은 데이터 변경, TCL은 트랜잭션 제어, DDL은 객체 정의, DCL은 권한 제어이며 일부 DBMS의 DDL 커밋 동작은 방언 전제를 확인한다.
- 계층형 질의는 루트·부모자식 방향·순방향/역방향 조건과 형제 순서를 따로 표시하고, PIVOT은 행 값을 열로 집계하는 변환이다.
시험에 바로 쓰는 비교표
| 항목 | 적용 시점 | 결과 행 | 판단 기준 |
|---|---|---|---|
| WHERE | 그룹화 전 | 조건 불일치 행 제거 | 일반 열 조건 |
| HAVING | 그룹화 후 | 조건 불일치 그룹 제거 | 집계 결과 조건 |
| COUNT(*) | 집계 시 | 모든 행 계산 | NULL 포함 행 수 |
| COUNT(열) | 집계 시 | 해당 열 NULL 제외 | 값이 존재하는 행 수 |
지문 표현을 판단 기준으로 바꾸기
- 비자명한 함수 종속의 결정자가 모두 슈퍼키다 → BCNF
- 외부 조인의 기준 방향 행을 보존한다 → OUTER JOIN
- 원래 행을 유지하면서 순위를 계산한다 → 윈도우 함수
SQLD · NULL과 서브쿼리
NOT IN을 썼는데 왜 한 행도 나오지 않을까?
NULL 하나를 추가했을 뿐인데 달라지는 조회 결과. 행별 판단표로 원인을 찾고 NOT EXISTS와 비교합니다.
01조건과 코드 읽기
candidate에서 excluded에 없는 id를 찾으려 합니다. candidate.id는 기본키여서 NULL이 없고, excluded.id는 NULL을 허용합니다. 여기서 NULL은 문자열 'NULL'이 아니라 값이 없는 상태입니다.
| id (기본키) |
|---|
| 11 |
| 22 |
| 33 |
| id (NULL 허용) |
|---|
| 22 |
| NULL |
SELECT id
FROM candidate
WHERE id NOT IN (
SELECT id FROM excluded
)
ORDER BY id;먼저 예상해보세요22를 제외한 11과 33이 나올까요? WHERE 조건이 TRUE가 되는 행을 골라보세요.
실행 결과와 풀이펼쳐보기접기
확인 결과
조회된 행 없음 (0행)
| id | id <> 22 | id <> NULL | AND 결과 | 조회 |
|---|---|---|---|---|
| 11 | TRUE | UNKNOWN | UNKNOWN | 제외 |
| 22 | FALSE | UNKNOWN | FALSE | 제외 |
| 33 | TRUE | UNKNOWN | UNKNOWN | 제외 |
이 스칼라 비교의 NOT IN (22, NULL)은 id <> 22 AND id <> NULL로 풀어 생각할 수 있습니다. NULL과의 일반 비교는 TRUE나 FALSE가 아니라 UNKNOWN입니다.
11과 33은 TRUE AND UNKNOWN이라 UNKNOWN, 22는 FALSE AND UNKNOWN이라 FALSE입니다. WHERE는 TRUE인 행만 남기므로 세 행이 모두 제외됩니다. NULL 때문에 SQL 문법 오류가 난 것은 아닙니다.
02다른 조건과 비교하기
비교 1 · 서브쿼리에서 NULL을 제외하면
SELECT id
FROM candidate
WHERE id NOT IN (
SELECT id FROM excluded
WHERE id IS NOT NULL
)
ORDER BY id;비교 1 · 서브쿼리에서 NULL을 제외하면 결과펼쳐보기접기
확인 결과
11 33
비교 집합이 22 하나가 됩니다. 11과 33은 22와 다르므로 남습니다. 원래 데이터의 NULL을 0 같은 임의의 값으로 바꿀 필요는 없습니다.
비교 2 · 일치하는 행이 없는지 검사하면
SELECT c.id
FROM candidate c
WHERE NOT EXISTS (
SELECT 1 FROM excluded e
WHERE e.id = c.id
)
ORDER BY c.id;비교 2 · 일치하는 행이 없는지 검사하면 결과펼쳐보기접기
확인 결과
11 33
각 candidate 행마다 e.id = c.id가 TRUE인 행을 찾습니다. 11과 33에는 일치 행이 없으므로 NOT EXISTS가 TRUE입니다. SELECT 1의 숫자를 합산하는 것이 아니라 서브쿼리 결과 행의 존재를 검사합니다.
03조건을 바꿔 다시 풀기
확인 1조회할 값에도 NULL이 있다면?
기본키 제약이 없는 별도의 candidate에 11, 22, 33, NULL이 있고 excluded는 22, NULL 그대로라고 가정하세요. 위 비교 1과 비교 2의 결과는 여전히 같을까요? 행 순서가 아니라 포함되는 값을 비교하세요.
확인 1 정답과 이유펼쳐보기접기
확인 결과
비교 1: 11, 33 비교 2: 11, 33, NULL
비교 1에서 NULL NOT IN (22)는 UNKNOWN이어서 제외됩니다. 비교 2에서는 NULL과 = 비교해 TRUE가 되는 행이 없어 NOT EXISTS가 TRUE입니다. 조회 대상의 NULL을 포함할지 먼저 정해야 두 SQL을 올바르게 선택할 수 있습니다.
확인 2비교할 집합이 비어 있다면?
excluded에 행이 하나도 없을 때, 기본 예제의 NOT IN 결과는 무엇일까요? 조회할 candidate는 원래의 11, 22, 33입니다.
확인 2 정답과 이유펼쳐보기접기
확인 결과
11 22 33
빈 집합에는 어떤 id와도 일치하는 값이 없습니다. NOT IN은 모든 행에서 TRUE여서 3행이 남습니다. 'NULL을 가진 행이 있는 집합'과 '행 자체가 없는 빈 집합'을 구별하세요.
이것만 기억하세요NOT IN은 비교 집합의 NULL을, NOT EXISTS는 일치 행의 존재를 확인합니다. 두 SQL을 바꿔 쓰기 전에는 조회 대상의 NULL 허용 여부도 함께 보세요.
직접 풀어보는 예제와 단계별 해설
같은 데이터에서 조건의 위치만 달라질 때 결과가 어떻게 바뀌는지 직접 비교합니다. 아래 데이터와 SQL은 정처LAB이 설계한 예제입니다.
LEFT JOIN의 ON과 WHERE는 바꿔 쓸 수 있을까?
모든 부서를 남기되 급여가 100 이상인 직원만 연결하려 합니다. 직원이 없는 부서도 결과에 있어야 합니다.
SQL
SELECT d.id, e.name
FROM dept d
LEFT JOIN emp e
ON e.dept_id = d.id AND e.salary >= 100
ORDER BY d.id, e.name;넓은 표는 좌우로 밀어 나머지 열을 볼 수 있습니다.
| id | name |
|---|---|
| 10 | 개발 |
| 20 | 운영 |
| 30 | 기획 |
| name | dept_id | salary |
|---|---|---|
| 민 | 10 | 120 |
| 솔 | 10 | 80 |
| 현 | 20 | 90 |
| 단계 | ON에 급여 조건 | WHERE에 급여 조건 |
|---|---|---|
| 10번 부서 | 민만 조건에 맞아 연결 | 민·솔 연결 후 솔 제거 |
| 20번 부서 | 적합한 직원 없음 → NULL 확장 행 보존 | 현 연결 후 90 >= 100이 거짓 → 행 제거 |
| 30번 부서 | 직원이 없어 NULL 확장 행 보존 | NULL >= 100이 UNKNOWN → 행 제거 |
| 최종 행 수 | 3 | 1 |
풀이 순서
- ON은 오른쪽 행이 조인에 성공하는 조건을 정합니다. 왼쪽 행에 매칭되는 직원이 없으면 LEFT JOIN이 NULL 확장 행을 만듭니다.
- 급여 조건을 WHERE로 옮기면 조인 결과를 만든 다음 필터링합니다. WHERE는 TRUE인 행만 남기므로 FALSE뿐 아니라 UNKNOWN인 행도 제거합니다.
확인 결과
10 | 민
20 | NULL
30 | NULL틀리기 쉬운 지점: LEFT JOIN을 썼다는 이유만으로 왼쪽의 모든 행이 최종 결과에 남는 것은 아닙니다. 뒤의 WHERE 조건까지 확인하세요.
COUNT(*)와 COUNT(오른쪽 열)의 차이
앞 예제의 ON 조건을 그대로 사용하여 부서별 적합한 직원 수를 구합니다. emp.name은 NULL이 없다고 가정합니다.
SQL
SELECT d.id, COUNT(*) AS joined_rows,
COUNT(e.name) AS employees
FROM dept d
LEFT JOIN emp e
ON e.dept_id = d.id AND e.salary >= 100
GROUP BY d.id
ORDER BY d.id;넓은 표는 좌우로 밀어 나머지 열을 볼 수 있습니다.
| 부서 | 조인 결과의 행 | COUNT(*) | COUNT(e.name) |
|---|---|---|---|
| 10 | 실제 직원 민 1행 | 1 | 1 |
| 20 | NULL 확장 행 1행 | 1 | 0 |
| 30 | NULL 확장 행 1행 | 1 | 0 |
풀이 순서
- COUNT(*)는 행 자체를 셉니다. 직원이 연결되지 않은 부서에도 조인 결과 행은 하나 있으므로 1이 됩니다.
- COUNT(e.name)은 해당 식이 NULL이 아닌 행만 셉니다. 이 예제에서는 적합한 직원 수를 세려는 목적에 맞습니다.
확인 결과
10 | 1 | 1
20 | 1 | 0
30 | 1 | 0틀리기 쉬운 지점: 원래부터 NULL을 허용하는 직원 이름 열이라면 실제 직원도 빠질 수 있습니다. 실제 설계에서는 NULL이 아닌 직원 식별자 열을 세는 편이 명확합니다.
다시 조인하면 복원되는데, 예약 충돌은 왜 못 막을까?
Booking(room, slot, team)은 방·시간대·팀을 기록합니다. 한 방의 같은 시간대에는 한 팀만 예약하고, 한 팀은 정해진 방 하나만 사용합니다. 함수 종속은 {room, slot} → team과 team → room 및 이들로부터 유도되는 것뿐입니다. 모든 값은 원자값이며 NULL과 중복 행은 없습니다. 모든 후보키와 3NF·BCNF 충족 여부를 판단한 뒤 TeamRoom(team, room), TeamSlot(team, slot)으로 나누면 무손실 분해와 종속성 보존을 모두 얻는지 예상하세요. SQL은 비어 있는 연습용 DB에서 실행합니다.
SQL
CREATE TABLE Booking (
room VARCHAR(1) NOT NULL,
slot VARCHAR(2) NOT NULL,
team VARCHAR(8) NOT NULL,
PRIMARY KEY (room, slot)
);
INSERT INTO Booking VALUES ('A', '09', 'Red');
INSERT INTO Booking VALUES ('A', '10', 'Red');
INSERT INTO Booking VALUES ('B', '09', 'Blue');
CREATE TABLE TeamRoom AS
SELECT DISTINCT team, room FROM Booking;
CREATE TABLE TeamSlot AS
SELECT DISTINCT team, slot FROM Booking;
SELECT r.room, s.slot, r.team
FROM TeamRoom r JOIN TeamSlot s ON r.team = s.team
ORDER BY r.room, s.slot, r.team;넓은 표는 좌우로 밀어 나머지 열을 볼 수 있습니다.
| 시작 속성 | 폐포 | 후보키 판단 |
|---|---|---|
| room | room | slot·team을 알 수 없음 |
| slot | slot | room·team을 알 수 없음 |
| team | team, room | slot을 알 수 없음 |
| room, slot | room, slot, team | 최소 슈퍼키 → 후보키 |
| team, slot | team, slot, room | 최소 슈퍼키 → 후보키 |
| room, team | room, team | slot을 알 수 없음 |
| 검사 | 이 예제의 근거 | 판정 |
|---|---|---|
| 3NF인가? | 모든 속성이 적어도 한 후보키에 포함됨 | 예 |
| BCNF인가? | team → room의 결정자 team은 슈퍼키가 아님 | 아니요 |
| 원래 유효한 데이터를 복원하는가? | 공통 속성 team이 TeamRoom의 모든 속성을 결정 | 무손실 |
| 각 표의 종속 검사만으로 충분한가? | 반례에서 A·09에 Blue와 Red가 함께 연결됨 | 종속성 미보존 |
| TeamRoom (team, room) | TeamSlot (team, slot) | 조인 결과 (room, slot, team) |
|---|---|---|
| Red, A | Red, 09 | A, 09, Red |
| Blue, A | Blue, 09 | A, 09, Blue |
풀이 순서
- slot은 어느 종속의 오른쪽에도 없으므로 모든 후보키에 필요합니다. 여기에 room 또는 team을 더하면 세 속성을 모두 구할 수 있고, 하나라도 빼면 안 됩니다. 후보키는 {room, slot}, {team, slot} 두 개뿐입니다. 표에 우연히 다른 유일한 열 조합이 보여도 업무 규칙에서 추가 종속을 추측하지 않습니다.
- room·slot·team 모두 주요 속성입니다. 따라서 모든 비자명 종속의 오른쪽 속성이 주요 속성이라는 3NF 조건을 만족합니다. 그러나 team만으로 slot을 알 수 없으므로 team → room은 BCNF 조건을 어깁니다.
- 두 분해 스키마의 공통 속성은 team이고 team → room이므로 공통 속성이 TeamRoom 전체를 결정합니다. 이것이 무손실 분해의 근거입니다. SQL의 원본 복원은 한 입력에 대한 실행 확인이며, 무손실 성질의 일반적 근거는 이 함수 종속입니다.
- 독립 반례에서는 각 팀에 방이 하나이고 각 (team, slot)도 유일합니다. 두 표가 각자의 종속을 지켜도 같은 (room, slot)에 두 팀이 예약됩니다. {room, slot} → team은 두 표의 종속만으로 보존되지 않으므로 조인 검사나 별도의 교차 표 제약이 필요합니다. 이것은 유효한 원본을 분해한 결과가 아니라, 분해된 표를 따로 갱신할 때 허용될 수 있는 상태입니다.
- 다시 풀기: 반례에서 Blue의 방만 B로 바꾸면 조인 결과는 (A, 09, Red), (B, 09, Blue)가 되어 이번 충돌은 사라집니다. 그렇다고 분해 자체가 종속성 보존으로 바뀌지는 않습니다. 종속성 보존은 한 데이터 상태가 아니라 스키마와 주어진 종속의 성질입니다.
확인 결과
A | 09 | Red
A | 10 | Red
B | 09 | Blue틀리기 쉬운 지점: 무손실은 원래의 유효한 데이터를 복원하는 성질이고, 종속성 보존은 분해된 표의 종속 검사로 원래 종속도 보장하는 성질입니다. SQL의 Booking 기본키는 일부 제약만 표현하며 team → room까지 자동 강제하지는 않습니다. 이 예제는 주어진 종속을 만족하는 입력에서 출발합니다.
검증 방법과 참고 문서
표의 동일 데이터를 로컬 메모리 SQL 엔진에 넣어 ON/WHERE의 두 결과와 집계 결과를 실행 검증합니다. 정규화 예제는 공개 SQL의 복원 결과와 독립 반례를 실행하고, 속성 폐포·모든 후보키·종속성 보존을 별도 계산으로 대조합니다. 유한한 입력의 실행 결과와 함수 종속에 근거한 일반적 판단을 구분합니다.
예제·데이터·풀이표: 정처LAB 자체 설계 · AI 보조 작성 및 로컬 검증 · 예제 수정일 2026-10-16
혼동하기 쉬운 지점
- NULL = NULL이 참이라고 판단
- HAVING을 항상 WHERE보다 먼저 적용
- 제2정규형과 제3정규형의 종속 제거 대상을 혼동
개념을 확인했다면 관련 문제에 적용해보세요.
첫 관련 문제 풀기