문제
풀이
SELECT ID, EMAIL, FIRST_NAME, LAST_NAME
FROM DEVELOPERS
WHERE SKILL_CODE & (SELECT SUM(CODE)
FROM SKILLCODES
WHERE CATEGORY = 'Front End')
ORDER BY ID ASC;
DEVELOPERS의 SKILL_CODE가 400 (0b1100010000)이면 SKILLCODES 테이블에서 CODE가 256 (0b100000000), 128 (0b10000000), 16 (0b10000)에 해당하는 스킬을 가졌음을 의미합니다.
이런 식으로 테이블을 구성하는 것은 비트 마스크를 활용한 것입니다. 비트 마스크를 활용하면 가독성을 해치고 구현이 어렵다는 단점이 존재합니다. 이와 관련해서는 다른 블로그의 좋은 글들을 참조하시기 바랍니다. (#1, #2, ...)
SKILLCODES 테이블에서 CATEGORY가 Front End인 레코드를 찾고, 그 레코드의 CODE 값을 모두 더합니다. CODE 열의 값은 2진수로 표현했을 때 각 비트로 구분될 수 있도록 2의 제곱수로 구성되어 있으므로 더하면 다음처럼 구성됩니다.
0b0000000100 (4)
+ 0b0001000000 (128)
= 0b0001000100 (132)
SKILLCODES 테이블에서 구한 값과 DEVELOPERS에 있는 레코드 각각의 SKILL_CODE를 비트 연산 AND 시 다음처럼 구성됩니다.
0b0000000100 (4)
& 0b0001000100 (132)
= 0b0000000100 (4)
0b0000000001 (1)
& 0b0001000100 (132)
= 0b0000000100 (0)
따라서 비트 연산 AND 시 0 이외의 수가 나오면 WHERE 절에서 참이 되어 해당 레코드가 선택됩니다. 반면 0이 나오면 해당 레코드는 선택되지 않게 됩니다.
다른 사람의 풀이
암시적 내부 조인
/*
* 이 소스 코드는 프로그래머스 seroak님의 소스 코드입니다.
* 원본을 확인하시려면 `https://school.programmers.co.kr/questions/72582`로 이동하세요.
*/
SELECT DISTINCT D.ID, D.EMAIL, D.FIRST_NAME, D.LAST_NAME
FROM DEVELOPERS D, SKILLCODES S
WHERE D.SKILL_CODE & S.CODE AND S.CATEGORY = 'Front End'
ORDER BY D.ID;
서브 쿼리를 사용하지 않고 암시적인 내부 조인 표현을 사용한 SQL 쿼리입니다.
DEVELOPERS 테이블의 레코드 각각의 SKILL_CODE과 SKILLCODES 테이블의 CATEGORY가 'Front End'인 레코드 각각의 CODE를 비트 연산 AND하여 참이 되는 레코드를 선택하게 됩니다.
다만 주의해야 할 점은 다음처럼 가상의 테이블이 구성되므로 DISTINCT를 통해 중복되는 레코드를 제거해야 합니다.
----------------------------------------------------------------------------------------------------------
| ID | FIRST_NAME | LAST_NAME | EMAIL | SKILL_CODE | NAME | CATEGORY | CODE |
----------------------------------------------------------------------------------------------------------
| D161 | Carsen | Garza | carsen_garza@grepp.co | 2048 | React | Front End | 2048 |
| D162 | Cade | Cunningham | cade_cunningham@grepp.co | 8452 | Vue | Front End | 8192 |
| D165 | Jerami | Edwards | jerami_edwards@grepp.co | 400 | JavaScript | Front End | 16 |
| D166 | Minha | Park | minha_park@grepp.co | 2324 | JavaScript | Front End | 16 |
| D166 | Minha | Park | minha_park@grepp.co | 2324 | React | Front End | 2048 |
----------------------------------------------------------------------------------------------------------
명시적 내부 조인
/*
* 이 소스 코드는 프로그래머스 백제완님의 소스 코드입니다.
* 원본을 확인하시려면 `https://school.programmers.co.kr/questions/72685`로 이동하세요.
*/
WITH FE AS (
SELECT
*
FROM
SKILLCODES
WHERE
CATEGORY = 'Front End'
)
SELECT
DISTINCT ID
, EMAIL
, FIRST_NAME
, LAST_NAME
FROM
DEVELOPERS A
INNER JOIN FE B
ON A.SKILL_CODE & B.CODE
ORDER BY
ID
SQL 쿼리 사용 시 서브 쿼리를 많이 사용하게 됩니다. 그러나 하나의 쿼리에서 서브 쿼리 여러 개가 등장하면 가독성이 떨어지며 재사용할 수 없어 동일한 쿼리를 작성해야 한다는 문제점이 생깁니다. 이러한 문제점을 해결하기 위해 WITH 문을 사용할 수 있습니다.
자주 사용되는 WITH 문은 대개 다음의 구조를 지니고 있습니다.
-- 단일 WITH 절
WITH [ 별명1 ] AS (
-- 서브 쿼리
)
-- 다중 WITH 절
WITH [ A별명1 ] AS (
-- 서브 쿼리
)
[ B별명1 ] AS (
-- 서브 쿼리
)
WITH 절을 사용해 위에서 언급한 가독성이 향상되고 재사용이 가능해지는 것에 더해 I/O 처리량이 많은 쿼리를 한 번만 실행해 임시 테이블에 저장하고 이 테이블을 다시 사용하므로 성능이 향상될 수 있습니다.
따라서 위 풀이의 소스 코드는 다음처럼 볼 수 있습니다.
SELECT DISTINCT ID, EMAIL, FIRST_NAME, LAST_NAME
FROM DEVELOPERS A
INNER JOIN (SELECT *
FROM SKILLCODES
WHERE CATEGORY = 'Front End'
) B
ON A.SKILL_CODE & B.CODE
ORDER BY ID;
DEVELOPERS 테이블의 레코드 각각의 SKILL_CODE과 SKILLCODES 테이블의 CATEGORY가 'Front End'인 레코드 각각의 CODE를 비트 연산 AND하여 참이 되는 레코드를 선택해 가상의 테이블을 만들고 이를 활용하고 있습니다.
프로그래머스 백제완님의 이 소스 코드에서도 프로그래머스의 seroak님 소스 코드에서 서술한 것처럼 가상의 테이블이 구성되므로 DISTINCT를 통해 중복되는 레코드를 제거해야 합니다.
참고 자료
- 프로그래머스의 seroak님: "왜 제가 쓴 코드는 안되는걸까요?" (2024.02.23.)
- 프로그래머스의 백제완님: "[MySQL] JOIN 혹은 비트연산 사용 답안" (2024.02.26.)
- Inpa Dev 👨💻의 인파님: "[MYSQL] 📚 WITH (임시 테이블 생성)" (2021.11.12.)