1. Problem
개발자들의 스킬 조합을 분석하여 등급(GRADE)을 매겨야 한다.
- A: Front End 스킬 AND Python 스킬 보유자
- B: C# 스킬 보유자
- C: 그 외의 Front End 스킬 보유자 단순히 특정 언어를 찾는 것이 아니라 'Front End'라는 카테고리 전체와 특정 언어를 교차 검증해야 하며, 등급 간의 우선순위를 정확히 배정하는 것이 핵심 과제이다.
2. Solution
WITH 절로 비트 마스크를 생성하고, 인라인 뷰를 통해 SQL 실행 순서 문제를 해결한다.
3. Takeaway (비트 연산의 효율성과 쿼리 실행 순서)
복합 조건의 등급 분류 쿼리를 작성하며 정립해야 할 객관적 원칙을 정리한다.
- 비트 마스크(Bit Mask)의 효율성: 여러 개의 Front End 코드를 하나하나 OR로 연결하는 대신, SUM(CODE)로 합쳐진 하나의 값과 & 연산을 수행함으로써 단 한 번의 연산으로 해당 카테고리 스킬 보유 여부를 판별할 수 있다.
- CASE WHEN의 전략적 배치: 등급 분류 시에는 가장 까다로운 조건(교집합이 있는 A등급)을 최상단에 배치해야 한다. 이는 하위 조건(C등급)에 포함될 수 있는 데이터를 상위 등급에서 먼저 가로채기(Short-circuit) 위함이다.
- SQL 실행 순서의 이해: WHERE 절은 SELECT 절보다 먼저 실행된다. 따라서 SELECT에서 CASE 문으로 만든 별칭(GRADE)을 필터링하려면 인라인 뷰나 CTE로 감싸서 실행 순서를 인위적으로 조정해야 한다.
- 묵시적 형변환(Implicit Conversion): 비트 연산의 결과는 숫자(0 또는 양수)이지만, SQL 조건절에서는 0을 FALSE로, 그 외의 숫자를 TRUE로 인지하는 특성을 활용하여 간결한 조건문을 완성할 수 있다.
https://school.programmers.co.kr/learn/courses/30/lessons/276036
프로그래머스
SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
SKILLCODES 테이블은 개발자들이 사용하는 프로그래밍 언어에 대한 정보를 담은 테이블입니다. SKILLCODES 테이블의 구조는 다음과 같으며, NAME, CATEGORY, CODE는 각각 스킬의 이름, 스킬의 범주, 스킬의 코드를 의미합니다. 스킬의 코드는 2진수로 표현했을 때 각 bit로 구분될 수 있도록 2의 제곱수로 구성되어 있습니다.
NAMETYPEUNIQUENULLABLE
| 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 테이블의 코드를 의미합니다.
NAMETYPEUNIQUENULLABLE
| 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 테이블에서 GRADE별 개발자의 정보를 조회하려 합니다. GRADE는 다음과 같이 정해집니다.
- A : Front End 스킬과 Python 스킬을 함께 가지고 있는 개발자
- B : C# 스킬을 가진 개발자
- C : 그 외의 Front End 개발자
GRADE가 존재하는 개발자의 GRADE, ID, EMAIL을 조회하는 SQL 문을 작성해 주세요.
결과는 GRADE와 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 테이블에 포함된 개발자들의 GRADE 및 정보가 결과에 나와야 합니다.
| GRADE | ID | |
| A | D162 | cade_cunningham@grepp.co |
| A | D165 | jerami_edwards@grepp.co |
| B | D164 | kelly_grant@grepp.co |
| C | D161 | carsen_garza@grepp.co |
1. 정답 쿼리
WITH SKILLS AS (
SELECT
(SELECT SUM(CODE) FROM SKILLCODES WHERE CATEGORY = "Front End") AS FE_CODE, # SUM으로 더하기
(SELECT CODE FROM SKILLCODES WHERE NAME = "Python") AS PYTHON_CODE,
(SELECT CODE FROM SKILLCODES WHERE NAME = "C#") AS C_SHARP_CODE
)
SELECT GRADE,ID, EMAIL
FROM ( # 인라인뷰를 쓰는 이유는 오직 WHERE절에서 GRADE NULL을 제거하기 위함
SELECT
CASE
WHEN (SKILL_CODE & (SELECT FE_CODE FROM SKILLS))
AND (SKILL_CODE & (SELECT PYTHON_CODE FROM SKILLS)) THEN "A" # 두 조건 모두 만족시 'A'
WHEN (SKILL_CODE & (SELECT C_SHARP_CODE FROM SKILLS)) THEN "B"
WHEN (SKILL_CODE & (SELECT FE_CODE FROM SKILLS)) THEN "C"
END AS GRADE,
ID,
EMAIL
FROM DEVELOPERS
) AS T # 인라인뷰 Alias 필수
WHERE GRADE IS NOT NULL # GRADE가 있는 개발자만 필터링
ORDER BY GRADE ASC, ID ASC;
1. 비트 연산(&)의 마법
이 문제의 핵심은 SKILL_CODE & CODE입니다.
- 스킬 코드가 2의 제곱수(1, 2, 4, 8...)로 되어 있다는 것은 이진수에서 **특정 자릿수(Bit)**가 켜져 있다는 뜻입니다.
- &(Bitwise AND) 연산을 하면, 개발자의 스킬 코드 안에 해당 언어의 비트가 포함되어 있을 때만 0이 아닌 값을 반환합니다.
- SKILL_CODE & FE_CODE: Front End 카테고리의 모든 코드를 합친(SUM) 값과 비트 연산을 수행함으로써, 여러 Front End 스킬 중 하나라도 가지고 있는지 한 번에 체크하는 아주 효율적인 방식을 선택하셨습니다.
2. 쿼리 구조 분석
① CTE (WITH SKILLS)
상수처럼 사용할 코드값들을 미리 정의했습니다.
- 특히 FE_CODE를 SUM(CODE)로 구한 것이 신의 한 수입니다. 비트 연산 특성상 여러 스킬 코드를 더한 값과 & 연산을 하면, 그중 하나라도 포함되어 있을 때 결과가 양수가 나오기 때문입니다.
② CASE WHEN의 우선순위
CASE 문은 위에서부터 순차적으로 조건을 검사하고, 조건이 만족되면 바로 종료됩니다.
- GRADE A: FE 스킬 AND Python을 가장 먼저 체크해야 합니다. (C와 겹칠 수 있기 때문)
- GRADE B: 그다음 순위인 C#을 체크합니다.
- GRADE C: 마지막으로 A가 아닌 개발자 중 FE 스킬이 있는 사람을 필터링합니다. 이 순서가 바뀌면 등급이 꼬일 수 있는데, 사용자님은 정확한 순서로 배치하셨습니다.
③ 인라인 뷰(T)와 WHERE 절
CASE 문에서 등급에 해당하지 않는 개발자는 ELSE 생략 시 NULL이 됩니다. 이를 인라인 뷰(FROM (SELECT...))로 감싸서 외부에서 WHERE GRADE IS NOT NULL로 걸러낸 것도 깔끔한 마무리입니다.
인라인 뷰를 쓴 이유는 SQL의 "엄격한 실행 순서" 때문입니다
SQL은 우리가 작성한 순서대로 실행되지 않습니다. 보통 다음과 같은 순서를 따릅니다:
- FROM / JOIN (어떤 테이블에서?)
- WHERE (어떤 행을 버릴까?)
- GROUP BY / HAVING (어떻게 묶을까?)
- SELECT (어떤 컬럼을 보여줄까? 여기서 GRADE 별칭이 생성됨)
- ORDER BY (어떻게 정렬할까?)
문제는 WHERE 절이 SELECT 절보다 먼저 실행된다는 점입니다. 그래서 컴퓨터는 WHERE GRADE IS NOT NULL을 맞닥뜨렸을 때, "아직 GRADE라는 게 뭔지 모르는데?"라며 에러를 냅니다.
이를 해결하기 위해 인라인 뷰로 한 번 감싸서 GRADE가 계산된 결과물을 '가상 테이블'로 만든 뒤, 그 바깥에서 WHERE 절을 적용한 것입니다.
3. 데이터 흐름 요약 (표)
개발자 D162를 예로 들어볼까요? (SKILL_CODE: 8452)
| 비교 대상 | 코드(2진수 예시) | 연산 결과 (8452 & CODE) | 결과값 |
| FE_CODE (SUM) | ...10100010000 | 존재함 (React, Vue 등) | TRUE |
| Python | ...00001000000 | 존재함 | TRUE |
| 최종 등급 | A (둘 다 TRUE이므로) | GRADE A |
인라인 뷰(서브쿼리) 안에 들어있는 CASE WHEN 절이 이 문제의 핵심이자 가장 복잡한 부분입니다. 차근차근 뜯어보면 의외로 간단한 '필터링 스위치' 구조로 되어 있습니다.
구조를 이해하기 쉽게 세 부분으로 나누어 설명해 드릴게요.
1. & 연산자가 하는 일 (스위치 확인)
SKILL_CODE & CODE는 **"이 개발자가 이 스킬을 가지고 있는가?"**를 묻는 질문입니다.
- 컴퓨터는 스킬을 이진수 스위치로 관리합니다. (예: 1번 스위치는 Python, 2번은 C#...)
- & 연산 결과가 0이 아니면 해당 스위치가 켜져(스킬이 있음) 있는 것이고, 0이면 꺼져(스킬이 없음) 있는 것입니다.
2. 조건의 우선순위 (왜 하필 A부터 검사할까?)
CASE WHEN은 위에서 아래로 흐릅니다. 첫 번째 조건(WHEN)이 맞으면 바로 등급을 매기고 다음 사람으로 넘어가 버립니다.
- A등급 조건 (교집합): (FE_CODE와 매칭) AND (Python과 매칭)
- 가장 까다로운 조건입니다. 두 스위치가 모두 켜져 있어야만 A를 줍니다.
- 두 검사가 **모두 참(TRUE)**일 때만 AND 연산이 최종적으로 참이 되어 THEN "A"를 실행합니다
WHEN (SKILL_CODE & (SELECT FE_CODE FROM SKILLS))
AND (SKILL_CODE & (SELECT PYTHON_CODE FROM SKILLS))
THEN "A"
- B등급 조건: (C#과 매칭): A등급이 아닌 사람 중에서 C# 스위치가 켜져 있다면 B를 줍니다.
- C등급 조건: (FE_CODE와 매칭): A도 아니고 B도 아닌데, Front End 스위치 중 하나라도 켜져 있다면 C를 줍니다.
💡 왜 C를 먼저 검사하면 안 되나요?
만약 C를 맨 위에 쓰면, 'FE + Python'을 가진 능력자도 그냥 C등급(Front End 개발자)으로 분류되고 끝나버립니다.
그래서 **범위가 좁고 까다로운 조건(A)**을 맨 위에 배치한 것입니다.
3. (SELECT FE_CODE FROM SKILLS)의 역할
괄호 안에 있는 이 쿼리는 앞서 WITH 절에서 미리 계산해둔 **"Front End 스킬들의 합계 점수"**를 가져오는 것입니다.
- 예를 들어 React(2048) + Vue(8192) + JS(16)가 Front End라면, FE_CODE는 이들을 다 더한 값이 됩니다.
- SKILL_CODE & FE_CODE라고 한 번만 써주면, 개발자가 이 셋 중 무엇을 가졌든 간에 하나라도 있으면 '참(True)'이 됩니다. 일일이 React가 있는가? OR Vue가 있는가?라고 묻지 않아도 되니 쿼리가 아주 깔끔해지죠.
🚩 오답노트: 나만의 비유로 이해하기
"인라인 뷰 안의 CASE 문은 채점기와 같다.
개발자마다 스킬 스위치를 확인해서 A, B, C 도장을 찍어준다. 만약 아무 조건에도 안 맞으면 도장을 찍지 않고(NULL), 밖의 WHERE 절에서 도장이 없는 사람들을 탈락시킨다."
결론부터 말씀드리면, **비트 연산(&) 자체의 결과는 Boolean(TRUE/FALSE)이 아니라 숫자(Integer)**입니다. 하지만 MySQL과 같은 일부 환경에서는 이 숫자가 조건문 안에서 묵시적으로 Boolean처럼 취급될 뿐입니다.
1. 연산 결과는 '숫자'입니다
SKILL_CODE & CODE를 실행하면 컴퓨터는 비트가 겹치는 자릿수의 값을 숫자로 반환합니다.
- 예시: 개발자의 SKILL_CODE가 5(b'101')이고, Python 코드가 4(b'100')라면?
- 5 & 4 ---> 결과는 4 (이진수 100)
- 예시: 개발자의 SKILL_CODE가 1(b'001')이고, Python 코드가 4(b'100')라면?
- 1 & 4 ---> 결과는 0
2. 그런데 왜 WHEN 절에서 작동하나요? (Implicit Conversion)
MySQL의 CASE WHEN이나 IF 같은 조건문에서는 숫자를 다음과 같이 해석하는 규칙이 있습니다.
- 0 : FALSE로 간주
- 0이 아닌 모든 숫자 (1, 4, 1024 등) : TRUE로 간주
그래서 사용자님이 작성하신 쿼리에서 WHEN (SKILL_CODE & FE_CODE)라고만 적어도, 연산 결과가 0보다 큰 숫자라면 SQL 엔진이 **"아, 이건 참(TRUE)이구나!"**라고 이해하고 등급을 매기는 것입니다.
'Data Science > SQL' 카테고리의 다른 글
| [SQL/오답] 기간 중복(Overlap)와 다중 테이블 조인을 활용한 대여 가능 차량 조회 (프로그래머스 Lv4) (0) | 2026.02.15 |
|---|---|
| [SQL/오답] 비트 연산자의 다대다 매칭과 EXISTS 성능 최적화 (프로그래머스 Lv4) (0) | 2026.02.15 |
| [SQL/오답] Self Join 대장균 3대 가계도 추적: 세대 고정 논리 (프로그래머스 Lv4) (0) | 2026.02.14 |
| [SQL/오답] 재귀 쿼리와 NOT IN의 함정: 멸종위기 개체 수 산출 (프로그래머스 Lv5) (0) | 2026.02.14 |
| [SQL/오답] DATE_FORMAT, INNER JOIN과 필터링: 게시글 vs 댓글 작성일 구분하기 (프로그래머스 Lv1) (0) | 2026.02.14 |