AP 과목 A #14: SQL 조인, 집계, NULL
응용정보기술자시험 과목 A에 필요한 SELECT·WHERE와 정렬, INNER JOIN과 LEFT JOIN, GROUP BY·HAVING과 COUNT, NULL과 3값 논리, NOT IN 함정, DISTINCT를 고정 데이터 추적 예제와 오리지널 연습 문제 5문항으로 익힙니다.
AP 과목 A 제14편 · 2026년도·실라버스 Ver.7.2 기준의 독립 학습 자료입니다. IPA와 제휴하지 않으며 합격을 보장하지 않습니다. 시리즈 허브 보기
IPA와 제휴·공인 관계가 없는 독립 학습 자료다. 기출문제와 제3자 해설을 사용하지 않았으며 합격을 보장하지 않는다.
학습 인덱스
- 이전: AP13 — 키와 정규화(여기서는 PK/FK로 표를 연결한다는 점만 사용)
- 이번: 공식 범위 · 용어 · 고정 데이터 · SELECT · 조인 · 그룹 · ON/WHERE · NULL · NOT IN · 중복/순서 · 요약 · 문제 · 복습
- 다음: AP15 — 트랜잭션·잠금·ACID(이번에는 다루지 않는다)
1. 공식 범위
출처: IPA 応用情報技術者試験(レベル3)シラバス Ver.7.2, https://www.ipa.go.jp/shiken/syllabus/omgdg50000005kq5-att/syllabus_ap_ver7_2.pdf (2026-10-21 확인, PDF 43–44쪽·인쇄 하단 번호 39–40쪽).
계층: 大分類3 技術要素 → 中分類9 データベース → 2. データベース設計 → (2) ③ データベースの論理設計(NULL), 그리고 3. データ操作 → (1) 관계 연산, (2) ② (b) 데이터 조작 언어(SELECT). SELECT/WHERE, 조인, 그룹·집계, NULL, 중복, 정렬이 범위다. AP13은 정규화된 키 연결만 제공하고, PDF 44쪽(인쇄 하단 번호 40쪽)에서 시작하는 트랜잭션 처리는 AP15로 미룬다.
2. 용어
| 한국어 | 일본어 | 영어 |
|---|---|---|
| 선택 / 조건절 | 選択 / WHERE句 | selection / WHERE clause |
| 내부 조인 | 内部結合 | inner join |
| 왼쪽 외부 조인 | 左外部結合 | left outer join |
| 그룹화 / 그룹 조건 | グループ化 / HAVING句 | grouping / HAVING clause |
| 집계 함수 | 集約関数 | aggregate function |
| 널(NULL) | NULL(ヌル) | NULL |
| 3값 논리 | 3値論理 | three-valued logic (3VL) |
| 중복 제거 | 重複排除 | duplicate elimination |
| 정렬 | 整列 | ordering |
| 상관 서브쿼리 | 相関副問合せ | correlated subquery |
3. 고정 데이터
DEPARTMENT(dept_id PK, dept_name)와 nullable FK인 EMPLOYEE.dept_id가 AP13의 PK/FK 연결이다. 정규화를 다시 설명하지 않는다.
| dept_id | dept_name |
|---|---|
| 10 | Sales |
| 20 | Dev |
| 30 | HR |
| 40 | Legal |
| emp_id | emp_name | dept_id | bonus |
|---|---|---|---|
| 1 | Aki | 10 | 100 |
| 2 | Bo | 10 | NULL |
| 3 | Chika | 20 | 200 |
| 4 | Dai | NULL | 50 |
| 5 | Emi | 20 | NULL |
| 6 | Fumi | 30 | 100 |
BLOCKED_DEPT(dept_id nullable)에는 (20), (NULL)만 있다. 논리적 처리 직관은 FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY다. 이는 의미를 이해하는 순서이지 DBMS의 물리 실행 순서를 주장하는 것이 아니다.
4. SELECT, WHERE, ORDER BY
SELECT emp_id, emp_name, bonus FROM EMPLOYEE
WHERE bonus >= 100
ORDER BY bonus DESC, emp_id ASC;
| emp_id | emp_name | bonus |
|---|---|---|
| 3 | Chika | 200 |
| 1 | Aki | 100 |
| 6 | Fumi | 100 |
WHERE는 TRUE만 남긴다. NULL 보너스와의 비교는 UNKNOWN이어서 Bo와 Emi가 빠진다. 두 번째 정렬 키가 100 동점을 확정한다.
5. INNER JOIN과 LEFT JOIN
SELECT e.emp_id, e.emp_name, d.dept_name
FROM EMPLOYEE AS e INNER JOIN DEPARTMENT AS d ON d.dept_id = e.dept_id
ORDER BY e.emp_id;
| emp_id | emp_name | dept_name |
|---|---|---|
| 1 | Aki | Sales |
| 2 | Bo | Sales |
| 3 | Chika | Dev |
| 5 | Emi | Dev |
| 6 | Fumi | HR |
내부 조인은 TRUE로 일치하는 행만 남긴다. Dai의 NULL = 값은 UNKNOWN이다.
SELECT e.emp_id, e.emp_name, d.dept_id, d.dept_name
FROM EMPLOYEE AS e LEFT JOIN DEPARTMENT AS d ON d.dept_id = e.dept_id
ORDER BY e.emp_id;
| emp_id | emp_name | dept_id | dept_name |
|---|---|---|---|
| 1 | Aki | 10 | Sales |
| 2 | Bo | 10 | Sales |
| 3 | Chika | 20 | Dev |
| 4 | Dai | NULL | NULL |
| 5 | Emi | 20 | Dev |
| 6 | Fumi | 30 | HR |
왼쪽 조인은 모든 왼쪽 행을 보존하고, 일치하지 않는 오른쪽 열을 NULL로 확장한다.
6. GROUP BY, HAVING, COUNT
이식 가능한 표준 방식대로 선택한 비집계 열을 모두 GROUP BY에 쓴다.
SELECT d.dept_id, d.dept_name, COUNT(*) AS joined_rows,
COUNT(e.emp_id) AS employee_count, COUNT(e.bonus) AS known_bonus_count
FROM DEPARTMENT AS d LEFT JOIN EMPLOYEE AS e ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name ORDER BY d.dept_id;
| dept_id | dept_name | joined_rows | employee_count | known_bonus_count |
|---|---|---|---|---|
| 10 | Sales | 2 | 2 | 1 |
| 20 | Dev | 2 | 2 | 1 |
| 30 | HR | 1 | 1 | 1 |
| 40 | Legal | 1 | 0 | 0 |
COUNT(*)는 행을, COUNT(식)은 식이 NULL이 아닌 행만 센다. Legal에는 NULL 확장 행 하나가 있어 1,0,0이다.
SELECT d.dept_id, d.dept_name, COUNT(e.emp_id) AS employee_count
FROM DEPARTMENT AS d LEFT JOIN EMPLOYEE AS e ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name
HAVING COUNT(e.emp_id) >= 2 ORDER BY d.dept_id;
| dept_id | dept_name | employee_count |
|---|---|---|
| 10 | Sales | 2 |
| 20 | Dev | 2 |
WHERE는 그룹화 전 행, HAVING은 집계 후 그룹을 거른다. 별칭을 HAVING에서 허용하는지는 제품마다 달라 집계식을 반복했다.
7. 오른쪽 조건: ON과 WHERE
ON의 조건은 오른쪽에서 무엇이 일치할지 정하면서 네 부서를 보존한다.
SELECT d.dept_id, d.dept_name, e.emp_name, e.bonus
FROM DEPARTMENT AS d LEFT JOIN EMPLOYEE AS e
ON e.dept_id = d.dept_id AND e.bonus >= 150
ORDER BY d.dept_id, e.emp_id;
| dept_id | dept_name | emp_name | bonus |
|---|---|---|---|
| 10 | Sales | NULL | NULL |
| 20 | Dev | Chika | 200 |
| 30 | HR | NULL | NULL |
| 40 | Legal | NULL | NULL |
SELECT d.dept_id, d.dept_name, e.emp_name, e.bonus
FROM DEPARTMENT AS d LEFT JOIN EMPLOYEE AS e ON e.dept_id = d.dept_id
WHERE e.bonus >= 150 ORDER BY d.dept_id, e.emp_id;
| dept_id | dept_name | emp_name | bonus |
|---|---|---|---|
| 20 | Dev | Chika | 200 |
두 번째는 조인 뒤 조건 불충족 행과 NULL 확장 행을 제거한다. 이 NULL 거부 오른쪽 조건은 결과상 내부 조인처럼 보이게 하지만, 모든 WHERE가 모든 LEFT JOIN을 내부 조인으로 만든다고 일반화하면 안 된다.
8. NULL과 3값 논리
| 식 | 결과 |
|---|---|
100 >= 100 |
TRUE |
NULL >= 100 |
UNKNOWN |
TRUE AND UNKNOWN |
UNKNOWN |
FALSE AND UNKNOWN |
FALSE |
TRUE OR UNKNOWN |
TRUE |
WHERE와 HAVING은 TRUE만 남긴다. = NULL, <> NULL은 NULL 검사가 아니라 결과가 UNKNOWN인 비교다. IS NULL, IS NOT NULL을 쓴다.
SELECT emp_id, emp_name FROM EMPLOYEE
WHERE bonus IS NULL ORDER BY emp_id;
| emp_id | emp_name |
|---|---|
| 2 | Bo |
| 5 | Emi |
9. NULL을 포함한 NOT IN
SELECT e.emp_id, e.emp_name FROM EMPLOYEE AS e
WHERE e.dept_id NOT IN (SELECT dept_id FROM BLOCKED_DEPT)
ORDER BY e.emp_id;
| emp_id | emp_name |
|---|---|
| (결과 없음) |
x NOT IN (20,NULL)은 개념적으로 x<>20 AND x<>NULL이다. 20은 FALSE, 그 밖의 값은 UNKNOWN, 직원의 NULL도 UNKNOWN이므로 TRUE가 하나도 없다.
SELECT e.emp_id, e.emp_name FROM EMPLOYEE AS e
WHERE e.dept_id NOT IN
(SELECT dept_id FROM BLOCKED_DEPT WHERE dept_id IS NOT NULL)
ORDER BY e.emp_id;
| emp_id | emp_name |
|---|---|
| 1 | Aki |
| 2 | Bo |
| 6 | Fumi |
Dai의 NULL NOT IN (20)은 여전히 UNKNOWN이다.
SELECT e.emp_id, e.emp_name FROM EMPLOYEE AS e
WHERE NOT EXISTS (SELECT 1 FROM BLOCKED_DEPT AS b
WHERE b.dept_id = e.dept_id)
ORDER BY e.emp_id;
| emp_id | emp_name |
|---|---|
| 1 | Aki |
| 2 | Bo |
| 4 | Dai |
| 6 | Fumi |
Dai에게 TRUE인 일치가 없으므로 NOT EXISTS는 TRUE다. 미상 부서를 제외하려는 업무 의도라면 e.dept_id IS NOT NULL을 추가한다.
SELECT e.emp_id, e.emp_name FROM EMPLOYEE AS e
WHERE e.dept_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM BLOCKED_DEPT AS b WHERE b.dept_id=e.dept_id)
ORDER BY e.emp_id;
| emp_id | emp_name |
|---|---|
| 1 | Aki |
| 2 | Bo |
| 6 | Fumi |
두 NOT EXISTS는 서로 다른 업무 의도를 표현한다.
10. 중복, DISTINCT, 정렬
SELECT dept_id FROM EMPLOYEE
WHERE dept_id IS NOT NULL ORDER BY dept_id;
| dept_id |
|---|
| 10 |
| 10 |
| 20 |
| 20 |
| 30 |
SELECT DISTINCT dept_id FROM EMPLOYEE
WHERE dept_id IS NOT NULL ORDER BY dept_id;
| dept_id |
|---|
| 10 |
| 20 |
| 30 |
SQL은 요청하지 않으면 중복을 보존한다. ORDER BY가 없으면 행 순서는 정의되지 않는다. 기본 NULL 위치와 NULLS FIRST/LAST 지원은 DBMS마다 다르다. 다음은 결정적 동점 키가 있는 이식 가능한 NULL-last 방식이다.
SELECT emp_id, emp_name, bonus FROM EMPLOYEE
ORDER BY CASE WHEN bonus IS NULL THEN 1 ELSE 0 END,
bonus DESC, emp_id ASC;
| emp_id | emp_name | bonus |
|---|---|---|
| 3 | Chika | 200 |
| 1 | Aki | 100 |
| 6 | Fumi | 100 |
| 4 | Dai | 50 |
| 2 | Bo | NULL |
| 5 | Emi | NULL |
예제는 SQLite에서 결정적으로 검증했다. 핵심 NULL·조인·COUNT 의미는 표준 SQL이지만 실행 계획, 정렬 규칙/대소문자 민감성, 기본 NULL 위치와 일부 문법은 DBMS마다 다르다.
11. 퀵시트와 함정
| 목적 | 기억할 점 |
|---|---|
| 행/그룹 필터 | 그룹 전 WHERE, 그룹 후 HAVING |
| 왼쪽 보존 | LEFT JOIN; 매치 제한은 의도에 따라 ON |
| 엔터티 수 | 무조건 COUNT(*)가 아니라 COUNT(오른쪽키) |
| NULL 검사 | IS NULL / IS NOT NULL |
| 제외 | NULL 의도를 정하고 NULL 포함 NOT IN 주의 |
| 중복/순서 | DISTINCT; 명시적 ORDER BY와 동점 키 |
함정: NULL을 0/빈 문자열로 생각하기, = NULL, 비집계 선택 열을 그룹화하지 않기, HAVING 별칭에 의존하기, 논리 순서를 물리 실행 순서로 오해하기, DISTINCT가 정렬한다고 믿기, 행에 자연 순서가 있다고 믿기.
12. 독창 연습문제
다음 5문항은 모두 독창적인 4지선다이며 기출문제가 아니다. 표시는 ア/イ/ウ/エ다.
Q1 — SELECT/WHERE
4절 쿼리의 직원 ID 순서는?
- ア.
1,3,6 - イ.
3,1,6 - ウ.
3,1,2,5,6 - エ.
3,6,1
정답: イ(2). ア는 보너스 내림차순을 무시한다. イ는 200 뒤 100 동점을 ID 오름차순으로 처리한다. ウ는 NULL을 포함한다. エ는 동점 순서를 뒤집는다.
Q2 — ON/WHERE
7절의 두 쿼리를 바르게 비교한 설명은?
- ア. 두 쿼리 모두 Dev/Chika만 반환한다.
- イ. WHERE 버전이 네 부서를 보존한다.
- ウ. ON 버전에는 Legal 행이 없다.
- エ. ON 버전은 부서 네 행, WHERE 버전은 Dev/Chika만 반환한다.
정답: エ(4). ア는 ON의 왼쪽 보존을 무시한다. イ는 반대다. ウ는 Legal의 NULL 확장 행을 놓친다. エ가 추적표와 같다.
Q3 — COUNT
Legal의 (COUNT(*),COUNT(e.emp_id),COUNT(e.bonus))는?
- ア.
(0,0,0) - イ.
(1,1,0) - ウ.
(1,0,0) - エ.
(1,0,1)
정답: ウ(3). ア는 NULL 확장 행을 잊었다. イ는 NULL emp_id를 센다. ウ는 행 하나와 non-NULL 식 0개를 바르게 센다. エ는 NULL bonus를 센다.
Q4 — NOT IN
9절 첫 쿼리의 행 수는?
- ア. 0
- イ. 3
- ウ. 4
- エ. 5
정답: ア(1). ア가 맞다. イ는 NULL 제거 서브쿼리 결과, ウ는 무가드 NOT EXISTS 결과, エ는 데이터로 뒷받침되지 않는다.
Q5 — 중복/순서
표준 SQL에서 옳은 설명은?
- ア. DISTINCT는 언제나 오름차순 정렬한다.
- イ. 정렬하면 non-NULL 부서값은
10,10,20,20,30이고, ORDER BY 없이는 순서가 미정이다. - ウ. 직원 PK가 투영된 dept_id 중복도 막는다.
- エ. NULL은 모든 DBMS에서 항상 마지막이다.
정답: イ(2). ア는 중복 제거와 정렬을 혼동한다. イ가 맞다. ウ는 emp_id 유일성과 dept_id 투영을 혼동한다. エ는 제품 차이를 무시한다.
정답표(1-based): [2,4,3,1,2] = [イ,エ,ウ,ア,イ].
13. 자가점검과 복습
실행하지 않고 모든 결과표, Legal의 1,0,0, NOT IN의 0행, Dai의 업무 의도 차이, 결정적 NULL-last 정렬을 설명해 보자.
- 1일: 고정 표를 그리고 WHERE·두 조인·COUNT를 손으로 추적한다.
- 3일: ON/WHERE와 세 제외 쿼리를 TRUE/FALSE/UNKNOWN으로 재현한다.
- 7일: 5문항을 자료 없이 풀고 GROUP/HAVING 쿼리와 결과표를 하나 작성한다.
출처·무결성: 공식 범위에는 위 IPA 실러버스만 사용했다. 기출문제와 제3자 해설은 사용하지 않았다.