1. Problem
사원 정보가 담긴 Employees 테이블과 급여 정보가 담긴 Salaries 테이블을 비교하여, 정보가 누락된 사원의 ID를 찾아야 한다. 누락의 기준은 '이름이 없거나' 혹은 '급여 정보가 없는' 경우이며, 이는 집합론적으로 두 집합의 합집합에서 교집합을 뺀 대칭차집합을 구하는 문제와 같다.
2. Solution
가장 직관적인 집계 방식부터 성능 최적화를 고려한 방식까지 총 세 가지 전략으로 접근할 수 있다.
[전략 A] 집계 중심: UNION ALL + HAVING (기발한 접근) 두 테이블의 ID를 수직으로 쌓은 뒤, 딱 한 번만 등장한 ID를 필터링한다. 조인 문법 없이도 명확하게 '혼자 있는 데이터'를 찾아낼 수 있다.
[전략 B] 논리 중심: NOT IN + UNION (표준적 접근) 각 테이블에서 상대방에게 없는 ID만 명확하게 골라내어 합친다. 문제의 조건을 쿼리 구조가 그대로 반영하고 있어 가독성이 뛰어나다.
[전략 C] 성능 중심: NOT EXISTS (고급 접근) 데이터 규모가 클 때 가장 효율적이다. 존재 여부만 확인하는 즉시 연산을 멈추는 'Early Exit' 특성 덕분에 대용량 데이터 환경에서 객관적인 우위를 점한다.
3. Takeaway (데이터 불일치 탐지 인사이트)
실무에서 두 시스템 간의 데이터가 맞지 않을 때 분석가가 취해야 할 기술적 태도를 정립한다.
- 상황에 맞는 도구 선택: 데이터 양이 적고 직관적인 확인이 필요할 때는 GROUP BY 방식이 훌륭하지만, 수억 건의 대용량 환경에서는 성능 최적화를 위해 NOT EXISTS를 우선적으로 고려해야 한다.
- NULL의 함정 방지: NOT IN은 서브쿼리에 NULL이 포함될 경우 예기치 않은 결과를 낼 수 있다. 데이터 무결성이 보장되지 않은 환경에서는 NOT EXISTS나 LEFT JOIN이 훨씬 안전한 선택지이다.
- 분석가의 무기, 대칭차집합: 인사 시스템과 급여 시스템처럼 서로 다른 출처의 데이터를 비교할 때, 대칭차집합은 데이터 품질을 검수하는 최후의 방어선이다. 단순히 조인만이 정답이 아니라는 사고의 유연함이 중요하다.
https://leetcode.com/problems/employees-with-missing-information/
Table: Employees
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| employee_id | int |
| name | varchar |
+-------------+---------+
employee_id is the column with unique values for this table.
Each row of this table indicates the name of the employee whose ID is employee_id.
Table: Salaries
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| employee_id | int |
| salary | int |
+-------------+---------+
employee_id is the column with unique values for this table.
Each row of this table indicates the salary of the employee whose ID is employee_id.
Write a solution to report the IDs of all the employees with missing information. The information of an employee is missing if:
The employee's name is missing, or
The employee's salary is missing.
Return the result table ordered by employee_id in ascending order.
The result format is in the following example.
Example 1:
Input:
Employees table:
+-------------+----------+
| employee_id | name |
+-------------+----------+
| 2 | Crew |
| 4 | Haven |
| 5 | Kristian |
+-------------+----------+
Salaries table:
+-------------+--------+
| employee_id | salary |
+-------------+--------+
| 5 | 76071 |
| 1 | 22517 |
| 4 | 63539 |
+-------------+--------+
Output:
+-------------+
| employee_id |
+-------------+
| 1 |
| 2 |
+-------------+
Explanation:
Employees 1, 2, 4, and 5 are working at this company.
The name of employee 1 is missing.
The salary of employee 2 is missing.
[SQL/Easy] Employees With Missing Information
1. 문제 풀이의 핵심: "대칭 차집합(Symmetric Difference)"
두 테이블을 비교하여 양쪽에 다 있지 않고 한쪽에만 누락된 ID를 찾는 것이 목표입니다.
- 방법 1 (사용자님 방식): 두 테이블의 ID를 일단 다 수직으로 쌓고(UNION ALL), 딱 한 번만 등장한 ID(COUNT = 1)를 찾는다.
- 방법 2 (전통적 방식): FULL OUTER JOIN을 수행한 후, 이름이나 급여가 NULL인 행을 찾는다.
2. 사용자 쿼리 분석 및 최적화
WITH summary AS(
-- 두 테이블의 ID를 모두 모음 (컬럼명/타입이 같아야 함)
SELECT employee_id FROM Employees
UNION ALL
SELECT employee_id FROM Salaries)
SELECT employee_id
FROM summary
GROUP BY employee_id
HAVING COUNT(employee_id) = 1 -- 두 번(양쪽) 등장하지 않은 ID만 필터링
ORDER BY employee_id ASC;
🔍 왜 이 방식이 훌륭한가요?
- 가독성: 조인 조건(ON s.id = e.id)이나 NULL 체크(IS NULL) 없이도 직관적으로 "혼자 있는 놈을 찾는다"는 논리가 명확합니다.
- 성능: 인덱스가 잘 잡혀 있다면 대량의 데이터를 조인하는 것보다 단순 수직 통합 후 그룹화하는 것이 더 빠를 때가 있습니다.
3. 출제자의 의도 분석
이 문제는 지원자가 **"데이터 불일치(Data Mismatch)"**를 어떻게 탐지하는지 확인합니다.
- 데이터 무결성 체크: 두 시스템(인사 시스템 vs 급여 시스템) 간의 데이터가 맞지 않을 때 이를 찾아낼 수 있는가?
- 집합 연산의 활용: UNION이나 JOIN을 상황에 맞게 선택하여 원하는 부분집합을 추출할 수 있는가?
- 정렬의 중요성: 결과가 늘어나는 순서(ASC)를 지켰는가?
오답노트: "조인만이 정답은 아니다"
"두 테이블의 차집합을 구할 때, UNION ALL + HAVING COUNT = 1 조합은 매우 강력한 무기다." 복잡한 JOIN 문법이 생각나지 않을 때, 데이터를 일단 한 바구니에 다 담고 개수를 세어보는 것만으로도 훌륭한 분석 결과를 낼 수 있습니다.
SQL의 성능과 표준적인 관점에서 가장 **'모범답안'**으로 꼽히는 방식은 UNION을 이용한 대칭 차집합(Symmetric Difference) 방식입니다.
이 방식은 각 테이블에서 상대방에게 없는 ID만 명확하게 골라내어 합치기 때문에, 데이터의 의도를 가장 잘 드러냅니다.
1. 모범 답안 (UNION 방식)
-- 1. 이름은 있는데 급여 정보가 없는 사람
SELECT employee_id
FROM Employees
WHERE employee_id NOT IN (SELECT employee_id FROM Salaries)
UNION
-- 2. 급여 정보는 있는데 이름이 없는 사람
SELECT employee_id
FROM Salaries
WHERE employee_id NOT IN (SELECT employee_id FROM Employees)
ORDER BY employee_id;
왜 이 쿼리가 '모범'인가?
- 가독성 (Readability): 문제에서 제시한 두 가지 조건("이름이 없거나", "급여가 없거나")을 쿼리 구조가 그대로 반영하고 있습니다. 나중에 다른 사람이 코드를 읽었을 때 의도를 파악하기 가장 쉽습니다.
- 인덱스 활용 (Performance): employee_id에 인덱스가 걸려 있다면, NOT IN이나 NOT EXISTS는 매우 빠르게 작동합니다. 사용자님의 GROUP BY 방식은 모든 데이터를 일단 합친 뒤 전체를 다시 훑어야(Full Scan) 하지만, 이 방식은 필요한 부분만 콕 집어낼 가능성이 높습니다.
- 결과 보장 (Robustness): UNION을 사용함으로써 혹시 모를 중복 ID 발생 가능성까지 원천 차단합니다.
2. 실무에서 더 선호하는 방식 (LEFT JOIN 활용)
현업에서는 LEFT JOIN 시 발생하는 NULL을 체크하는 방식을 선호하기도 합니다. 이는 데이터가 비어있는 '상태'를 직접 확인할 수 있기 때문입니다.
SELECT T.employee_id
FROM (
SELECT employee_id FROM Employees
UNION
SELECT employee_id FROM Salaries
) T
LEFT JOIN Employees E ON T.employee_id = E.employee_id
LEFT JOIN Salaries S ON T.employee_id = S.employee_id
WHERE E.name IS NULL OR S.salary IS NULL
ORDER BY T.employee_id;
비교 요약
| 방식 | 특징 | 추천 상황 |
| UNION ALL + GROUP BY (사용자님) | 기발함, 조인이 필요 없음 | 데이터 양이 적고 직관적인 집계가 필요할 때 |
| NOT IN + UNION (모범 답안) | 성능 최적화, 논리적 명확성 | 실제 코딩 테스트 및 대용량 데이터 처리 |
| LEFT JOIN + NULL 체크 | 데이터 불일치 상세 확인 가능 | 누락된 데이터의 구체적인 컬럼을 같이 보고 싶을 때 |
3. 성능의 제왕: NOT EXISTS
NOT IN은 서브쿼리의 모든 데이터를 가져와서 일일이 비교하지만, **NOT EXISTS**는 조건에 맞는 데이터를 찾는 즉시 멈춥니다(Early Exit). 데이터가 많아질수록 이 차이는 엄청나게 커지죠.
✅ NOT EXISTS를 이용한 모범 답안
-- 1. 이름은 있는데 급여 정보가 존재하지 않는 경우
SELECT employee_id
FROM Employees E
WHERE NOT EXISTS (
SELECT 1 FROM Salaries S WHERE S.employee_id = E.employee_id
)
UNION
-- 2. 급여 정보는 있는데 이름이 존재하지 않는 경우
SELECT employee_id
FROM Salaries S
WHERE NOT EXISTS (
SELECT 1 FROM Employees E WHERE E.employee_id = S.employee_id
)
ORDER BY employee_id;
💡 왜 NOT EXISTS가 더 좋을까?
- NULL에 강함: NOT IN은 서브쿼리에 NULL이 하나라도 섞여 있으면 결과가 통째로 안 나올 수 있는 위험이 있지만, NOT EXISTS는 NULL에 영향을 받지 않고 정확하게 작동합니다.
- 연산 효율: "있는지 없는지만 확인하면 끝!"이라는 논리라 DB 엔진이 가장 좋아하는 방식입니다.
- SELECT 1: 서브쿼리에서 실제 컬럼 값을 가져올 필요가 없으므로 관습적으로 1을 써서 부하를 줄입니다.
"대용량 데이터에서 누락된 정보를 찾을 때는 NOT EXISTS를 떠올리세요."
초보자는 NOT IN을 쓰고,
중급자는 LEFT JOIN을 쓰며,
고수는 **NOT EXISTS**로 성능까지 챙깁니다.
'Data Science > SQL' 카테고리의 다른 글
| [SQL/오답] Recursive CTE와 정규표현식을 활용한 커스텀 텍스트 포맷팅 (LeetCode3374 Hard) (0) | 2026.02.16 |
|---|---|
| [SQL/오답] 조건부 집계(CASE WHEN)와 집합 결합을 활용한 홀짝 거래액 산출 (LeetCode3220 Medium) (0) | 2026.02.16 |
| [SQL/오답] AVG(조건식) 승인율 계산과 NULL 처리 (LeetCode1934 Medium) (0) | 2026.02.16 |
| [뉴스] DB까지 자연어로 가능한 시대 <다큐브, 예일대 'AI모델 검증' 벤치마크 1위 달성…中 텐센트 제쳤다> (0) | 2026.02.16 |
| [SQL/오답] 데이터가 없어도 항목 유지: UNION ALL (LeetCode1907 Medium) (0) | 2026.02.16 |