1. Problem
개발자의 스킬 정보가 2의 거듭제곱수들의 합(SKILL_CODE)으로 저장되어 있는 환경에서 'Front End' 카테고리에 속한 스킬을 하나라도 보유한 개발자를 추출해야 한다. 정수 내부에 숨겨진 비트 정보를 해석해야 하며, 한 개발자가 여러 개의 프론트엔드 스킬을 가졌을 때 발생하는 데이터 중복 문제를 깔끔하게 해결하는 것이 핵심이다.
2. Solution
비트 AND(&) 연산자를 활용해 포함 관계를 확인하고, 데이터 중복 방지 및 성능을 위해 EXISTS 문을 사용한다.
[권장 전략] EXISTS를 활용한 효율적 탐색
- 비트 연산 (D.SKILL_CODE & S.CODE) > 0: 개발자의 코드와 스킬 코드를 이진수로 대조하여 겹치는 비트가 존재할 경우(보유 스킬일 경우) 참을 반환한다.
- EXISTS의 메커니즘: 조건에 부합하는 행이 단 하나라도 발견되면 즉시 TRUE를 반환하고 다음 개발자로 넘어가므로 DISTINCT 없이도 중복을 원천 차단한다.
3. Takeaway (비트 마스킹과 세미 조인의 가치)
고급 쿼리 설계 과정에서 분석가가 확보해야 할 기술적 통찰을 정리한다.
- 비트 마스킹(Bit Masking)의 효율성: 여러 상태 값을 하나의 정수로 관리하는 비트 구조는 저장 공간을 절약하지만 조회가 까다롭다. 이때 비트 연산자를 조인 조건이나 필터링에 직접 녹여내는 능력은 복잡한 데이터 구조를 다루는 필수 역량이다.
- EXISTS vs JOIN + DISTINCT: 데이터 규모가 커질수록 모든 행을 복제한 뒤 다시 합치는 DISTINCT 방식보다, 존재 여부만 확인하고 멈추는 세미 조인(EXISTS) 방식이 메모리와 연산 속도 측면에서 객관적인 우위를 점한다.
- 상관 서브쿼리의 최적화: 바깥쪽 쿼리의 행을 하나씩 검사하는 상관 서브쿼리 구조를 이해함으로써, 대량의 로그 데이터에서 특정 조건을 만족하는 유저를 효율적으로 추려내는 실무적 감각을 익힐 수 있다.
SKILLCODES 테이블은 개발자들이 사용하는 프로그래밍 언어에 대한 정보를 담은 테이블입니다. SKILLCODES 테이블의 구조는 다음과 같으며, NAME, CATEGORY, CODE는 각각 스킬의 이름, 스킬의 범주, 스킬의 코드를 의미합니다. 스킬의 코드는 2진수로 표현했을 때 각 bit로 구분될 수 있도록 2의 제곱수로 구성되어 있습니다.
| NAME | TYPE | UNIQUE | NULLABLE |
| NAME | VARCHAR(N) | Y | N |
| CATEGORY | VARCHAR(N) | N | N |
| CODE | INTEGER | Y | N |
DEVELOPERS 테이블은 개발자들의 프로그래밍 스킬 정보를 담은 테이블입니다. DEVELOPERS 테이블의 구조는 다음과 같으며, ID, FIRST_NAME, LAST_NAME, EMAIL, SKILL_CODE는 각각 개발자의 ID, 이름, 성, 이메일, 스킬 코드를 의미합니다. SKILL_CODE 컬럼은 INTEGER 타입이고, 2진수로 표현했을 때 각 bit는 SKILLCODES 테이블의 코드를 의미합니다.
| NAME | TYPE | UNIQUE | NULLABLE |
| ID | VARCHAR(N) | Y | N |
| FIRST_NAME | VARCHAR(N) | N | Y |
| LAST_NAME | VARCHAR(N) | N | Y |
| VARCHAR(N) | Y | N | |
| SKILL_CODE | INTEGER | N | N |
예를 들어 어떤 개발자의 SKILL_CODE가 400 (=b'110010000')이라면, 이는 SKILLCODES 테이블에서 CODE가 256 (=b'100000000'), 128 (=b'10000000'), 16 (=b'10000') 에 해당하는 스킬을 가졌다는 것을 의미합니다.
문제
DEVELOPERS 테이블에서 Front End 스킬을 가진 개발자의 정보를 조회하려 합니다. 조건에 맞는 개발자의 ID, 이메일, 이름, 성을 조회하는 SQL 문을 작성해 주세요.
결과는 ID를 기준으로 오름차순 정렬해 주세요.
예시
예를 들어 SKILLCODES 테이블이 다음과 같고,
| NAME | CATEGORY | CODE |
| C++ | Back End | 4 |
| JavaScript | Front End | 16 |
| Java | Back End | 128 |
| Python | Back End | 256 |
| C# | Back End | 1024 |
| React | Front End | 2048 |
| Vue | Front End | 8192 |
| Node.js | Back End | 16384 |
DEVELOPERS 테이블이 다음과 같다면
| ID | FIRST_NAME | LAST_NAME | SKILL_CODE | |
| D165 | Jerami | Edwards | jerami_edwards@grepp.co | 400 |
| D161 | Carsen | Garza | carsen_garza@grepp.co | 2048 |
| D164 | Kelly | Grant | kelly_grant@grepp.co | 1024 |
| D163 | Luka | Cory | luka_cory@grepp.co | 16384 |
| D162 | Cade | Cunningham | cade_cunningham@grepp.co | 8452 |
다음과 같이 DEVELOPERS 테이블에 포함된 개발자 중 Front End 스킬을 가진을 가진 개발자의 정보가 결과에 나와야 합니다.
| ID | FIRST_NAME | LAST_NAME | |
| D161 | carsen_garza@grepp.co | Carsen | Garza |
| D162 | cade_cunningham@grepp.co | Cade | Cunningham |
| D165 | jerami_edwards@grepp.co | Jerami | Edwards |
1. 정답쿼리
SELECT DISTINCT D.ID, D.EMAIL, D.FIRST_NAME, D.LAST_NAME # 여러 FrontEnd 스킬 갖더라도 1번만 출력
FROM DEVELOPERS D
JOIN SKILLCODES S ON (D.SKILL_CODE & S.CODE) > 0 # 스킬코드 교집합이 1개라도 있으면 조인
WHERE S.CATEGORY = 'Front End' # 스킬코드가 Front End 인 것만 필터링
ORDER BY D.ID ASC;
[SQL/오답노트] 비트 연산자를 활용한 다대다 조건 필터링
1. 문제 핵심
- 핵심 타겟: CATEGORY = 'Front End'에 해당하는 스킬을 하나라도 가진 개발자 찾기.
- 기술적 난관: 한 명의 개발자가 여러 스킬(SKILL_CODE)을 숫자의 합 형태로 가지고 있음.
2. 해결의 열쇠: 비트 AND (&) 연산
개발자의 SKILL_CODE와 스킬의 CODE를 비트 단위로 대조하여, 겹치는 비트가 있는지 확인하는 것이 포인트입니다.
- 연산 로직: (D.SKILL_CODE & S.CODE) > 0
- 결과가 0보다 크다 = 해당 스킬의 비트가 개발자의 코드 안에 포함되어 있다(스킬 보유).
- 결과가 0이다 = 해당 스킬이 없다.
3. 왜 JOIN 조건에 비트 연산을 썼을까?
보통 ON A.ID = B.ID 같은 일치 조건만 생각하기 쉽지만, 이 문제처럼 **"포함 관계"**를 확인해야 할 때는 조인 조건에 비트 연산식을 직접 넣을 수 있습니다.
- JOIN 과정:
- 개발자 한 명당 모든 스킬 코드를 대조해본다.
- 그중 비트가 겹치는 스킬들만 옆에 가져다 붙인다.
- WHERE 절에서 'Front End' 카테고리만 남긴다.
4. DISTINCT가 필수인 이유
한 개발자가 'React'와 'Vue'라는 두 개의 Front End 스킬을 가지고 있다면, 조인 결과 데이터가 두 줄이 생기게 됩니다. 문제에서는 개발자의 정보를 한 번만 보여주길 원하므로 중복을 제거하는 DISTINCT가 반드시 필요합니다.
2. 다른 풀이
SELECT D.ID, D.EMAIL, D.FIRST_NAME, D.LAST_NAME
FROM DEVELOPERS D
WHERE EXISTS (
SELECT 1
FROM SKILLCODES S
WHERE S.CATEGORY = 'Front End'
AND (D.SKILL_CODE & S.CODE) > 0
)
ORDER BY D.ID ASC;
💡 DISTINCT의 대안, EXISTS 활용법
JOIN과 DISTINCT를 사용하면 일단 데이터를 다 합친 뒤에 중복을 찾아 지우는 과정을 거칩니다. 반면, EXISTS는 "조건에 맞는 게 하나라도 있어? 그럼 통과!" 하고 바로 다음 개발자로 넘어가기 때문에 훨씬 효율적일 수 있습니다.
🔍 왜 이 방법이 더 좋을까요? (성능 포인트)
- 중복 발생 원천 차단: JOIN은 한 개발자가 Front End 스킬 3개를 가졌을 때 데이터가 3배로 불어나지만, EXISTS는 조건을 만족하는 스킬을 찾는 순간 검사를 멈추고 해당 개발자를 결과에 포함시킵니다. 따라서 DISTINCT를 쓸 필요가 없습니다.
- 메모리 효율: 데이터가 수백만 건일 때 모든 데이터를 JOIN해서 메모리에 올린 뒤 중복을 제거하는 것은 부담이 큽니다. EXISTS는 세미 조인(Semi-join) 방식으로 동작하여 메모리 사용량을 줄여줍니다.
- 가독성: "Front End 스킬을 가진 개발자만 뽑는다"라는 문제의 의도가 WHERE EXISTS 문구에 더 직관적으로 드러납니다.
🚩 오답노트:
"데이터가 많아질수록 JOIN + DISTINCT 조합보다는 EXISTS를 먼저 떠올리자."
분석 실무에서는 수억 건의 로그를 다룰 때가 많습니다. 이때 무심코 쓴 DISTINCT 때문에 쿼리가 무한 대기에 빠질 수 있는데, EXISTS는 이런 성능 병목을 해결해주는 훌륭한 도구입니다.
WHERE EXISTS (
SELECT 1
FROM SKILLCODES S
WHERE S.CATEGORY = 'Front End'
AND (D.SKILL_CODE & S.CODE) > 0
)
- 메인 쿼리 실행: DEVELOPERS 테이블에서 첫 번째 개발자(D161)를 집어듭니다.
- 서브쿼리 가동: 이 개발자의 SKILL_CODE를 들고 SKILLCODES 테이블로 들어갑니다.
- 조건 대조: SKILLCODES에 있는 스킬들 중 카테고리가 'Front End'이고, 이 개발자가 가진 비트(&)가 포함된 게 있는지 하나씩 확인합니다.
- 즉시 종료: 만약 'JavaScript'라는 스킬이 조건에 딱 맞으면, 서브쿼리는 더 이상 다른 스킬(React, Vue 등)을 찾아보지 않고 즉시 "있음(TRUE)"을 메인 쿼리에 보고하고 종료합니다.
1. 왜 SELECT 1인가요?
서브쿼리 안에 SELECT ID나 SELECT NAME이 아니라 SELECT 1을 쓴 것을 보셨을 거예요.
- EXISTS는 **"데이터가 존재하느냐 아니냐"**라는 사실만 궁금해합니다.
- 안에 어떤 컬럼값이 들어있는지는 전혀 중요하지 않기 때문에, 관습적으로 가장 가벼운 값인 1을 사용합니다. (사실 SELECT *을 써도 DB 엔진이 알아서 최적화하지만, 1을 쓰는 것이 "존재 여부만 보겠다"는 의도를 명확히 전달합니다.)
2. 상관 서브쿼리(Correlated Subquery)의 특징
이 서브쿼리 안에는 D.SKILL_CODE라는 바깥쪽 테이블(DEVELOPERS)의 컬럼이 들어있습니다. 이렇게 안쪽 쿼리가 바깥쪽 쿼리의 값을 참조하는 것을 '상관 서브쿼리'라고 합니다.
- 바깥쪽 개발자가 바뀔 때마다 안쪽 서브쿼리의 기준값(D.SKILL_CODE)도 계속 바뀝니다.
- 마치 루프(Loop) 문처럼 "개발자 A씨, 당신의 스킬 중 Front End가 있나요?", **"개발자 B씨, 당신은요?"**라고 한 명씩 물어보는 구조입니다.
3. JOIN과 결정적인 차이점 (중복 방지)
이 부분이 가장 중요합니다.
- JOIN: 조건에 맞는 게 3개 있으면 결과 행을 3개로 복사해서 옆에 붙입니다. (그래서 나중에 DISTINCT로 다시 합쳐야 하죠.)
- EXISTS: 조건에 맞는 게 1개든 100개든, "있다"는 사실이 확인되는 순간 메인 쿼리의 해당 개발자 행을 단 한 번만 결과에 포함시킵니다.
🚩 '한 줄 정리'
"EXISTS 서브쿼리는 데이터의 내용을 가져오는 도구가 아니라, 데이터의 존재 증명서다.
조건에 맞는 데이터를 단 하나라도 발견하는 즉시 탐색을 멈추기 때문에 성능과 중복 방지 측면에서 매우 유리하다."
'Data Science > SQL' 카테고리의 다른 글
| [SQL/오답] 기간별 할인 로직 구현; WITH 절과 LEFT JOIN의 조화 (프로그래머스 Lv4) (1) | 2026.02.15 |
|---|---|
| [SQL/오답] 기간 중복(Overlap)와 다중 테이블 조인을 활용한 대여 가능 차량 조회 (프로그래머스 Lv4) (0) | 2026.02.15 |
| [SQL/오답] 비트 연산으로 다대다 스킬 매칭: 비트 마스크와 인라인 뷰 (프로그래머스 Lv4) (1) | 2026.02.15 |
| [SQL/오답] Self Join 대장균 3대 가계도 추적: 세대 고정 논리 (프로그래머스 Lv4) (0) | 2026.02.14 |
| [SQL/오답] 재귀 쿼리와 NOT IN의 함정: 멸종위기 개체 수 산출 (프로그래머스 Lv5) (0) | 2026.02.14 |