SQL 문제 해결 회고
프로그래머스 '조건에 맞는 개발자 찾기' 문제를 풀며, 일반적인 외래키(FK) 매핑이 아닌 비트 연산(Bitwise Operation)을 통한 조인 기법을 학습했다. 더 나아가,JOIN연산 시 필연적으로 발생하는 '1:N 데이터 증식(뻥튀기)' 문제를 해결하기 위해DISTINCT를 사용한 사후 처리 방식과EXISTS를 사용한 사전 차단 방식의 아키텍처 차이를 깊이 있게 분석해 본다.
하나의 정수(SKILL_CODE) 안에 여러 개의 스킬 상태를 구겨 넣는 비트 마스킹 방식에서는 일반적인 등호(=)로 조인할 수 없다. 일치 여부가 아니라 '교집합'을 구해야 하므로, 비트 단위 연산자 & (AND)를 사용해야 한다.
(D.SKILL_CODE & S.CODE) = S.CODE& 연산했을 때, 원본 스킬 코드가 그대로 나온다면 해당 스킬을 보유하고 있다는 뜻이다.JOIN + DISTINCT (사후 처리)가장 직관적인 방법은 비트 연산을 조건으로 JOIN을 수행하는 것이다.
SELECT DISTINCT D.ID, D.EMAIL, D.FIRST_NAME, D.LAST_NAME
FROM DEVELOPERS AS D
JOIN SKILLCODES AS S ON (D.SKILL_CODE & S.CODE) = S.CODE
WHERE S.NAME IN ('Python', 'C#')
ORDER BY D.ID ASC;
DISTINCT가 필수적인가?조인을 수행하는 순간, 압축되어 있던 코드가 풀리며 개발자 1명의 데이터가 자신이 보유한 스킬 개수만큼 여러 줄로 복제(증식)된다. 만약 어떤 개발자가 Python과 C#을 모두 할 줄 안다면, 조인된 거대한 임시 테이블에는 그 개발자의 행이 2줄 생성된다. 이 상태로 SELECT를 하면 동일한 개발자가 두 번 출력되는 논리적 오류가 발생하므로, 마지막에 DISTINCT로 중복을 제거해 주어야만 정답이 된다.
하지만 데이터가 수천만 건이라면? 쓸데없이 데이터를 1:N으로 복제해 놓고 다시 압축하는 것은 심각한 메모리 낭비를 초래한다.
EXISTS (원천 봉쇄)JOIN이 두 테이블을 물리적으로 '결합'하여 데이터를 증식시킨다면, EXISTS는 결합하지 않고 서브쿼리를 통해 '존재 여부'만 질문하는 방식이다.
SELECT D.ID, D.EMAIL, D.FIRST_NAME, D.LAST_NAME
FROM DEVELOPERS AS D
WHERE EXISTS (
SELECT 1
FROM SKILLCODES AS S
WHERE S.NAME IN ('Python', 'C#')
AND (D.SKILL_CODE & S.CODE) = S.CODE
)
ORDER BY D.ID ASC;
EXISTS는 서브쿼리 내에서 조건에 맞는 데이터(예: Python)를 딱 하나라도 발견하는 순간, 그 뒤의 데이터(예: C# 보유 여부)는 더 이상 쳐다보지도 않고 탐색을 즉시 종료(True 반환)한다. 불필요한 연산을 칼같이 끊어버린다.DEVELOPERS 테이블의 데이터가 다른 테이블과 결합하지 않으므로, 원본 데이터가 단 한 줄도 복제되지 않는다. 애초에 중복이 발생하지 않으므로 무거운 DISTINCT 연산을 수행할 필요가 전혀 없다.원칙: 대상 테이블(A)의 컬럼만 출력해야 하는데, 조건 확인을 위해 1:N 관계인 참조 테이블(B)을 뒤져야 한다면, 무지성
JOIN + DISTINCT를 쓰기 전에EXISTS서브쿼리로 탐색 공간을 소거할 수 있는지 먼저 검토하라.