문제 설명

 

문제


풀이(답)

WITH RECURSIVE generations AS (  # 1. 재귀함수 정의
	SELECT				# 2. Anchor member: 부모가 없는(최초) 1 세대
    	id,
        parent_id,
        1 AS generation		# 3. 1 세대를 의미
    FROM ECOLI_DATA
    WHERE parent_id IS NULL 	# 4. 부모가 NULL이면 최상위 세대
    
    UNION ALL					# 5. Anchor 결과와 Recursive 결과를 합침
    
    SELECT				# 6. Recursive member: 이미 찾은 세대의 자식들을 찾아 세대를 +1 해가며 확장
    	child.id,
        child.parent_id
        parent.generation + 1 as generation	# 7. 부모 세대 +1 을 자식 세대로 지정
    FROM ECOLI_DATA AS child JOIN generations as parent ON child.parent_id = parent.id
    # 8. 전체 데이터(child)에서 기존의 계산된 generations(parent)의 id와 child.parent_id가 
    # 일치하는 자식만 선택
)
# 9. 재귀 완성 후, generations를 이용해 결과 조회
SELECT
	COUNT(*) AS COUNT
    g.generation AS GENERATION
FROM generations AS g
WHERE NOT EXISTS (	# 10. NOT EXISTS를 사용해 '자식이 없는' 레코드만 걸러냄
	SELECT GENERATION
    FROM ECOLI_DATA AS e2
    WHERE e2.parent_id = g.id	# 11. 이 ID를 부모로 가진 레코드가 하나도 없는 경우
)
GROUP BY g.generation
ORDER BY g.generation

📌 문제에 대한 접근 방법

이 문제는 ECOLI_DATA 테이블에서 계층 구조(부모-자식 관계)를 다뤄야 하는 문제이다. 특히, 특정 개체의 자식 세대가 여러 단계로 확장되어 있어서 몇 세대까지 존재할지 모르는 경우, 단순 JOIN만으로는 처리하기 어렵다.

이런 상황에서는 다음의 두 가지를 활용한다:

  • WITH RECURSIVE
    깊이를 알 수 없는 트리 구조(부모-자식 관계)를 SQL만으로 해결할 때 사용하는 방법이다. 최초 기준(부모가 없는 개체)을 세우고, 자기 자신과 JOIN을 반복(재귀 호출)하면서 다음 세대, 그 다음 세대까지 무한히 확장해 나갈 수 있다.
  • UNION ALL
    위에서 만든 최초 기준(Anchor member)의 결과와 재귀 호출(Recursive member)의 결과를 하나로 합치는 역할을 한다.
    UNION ALL은 단순히 결과를 붙이는 연산으로 중복 제거가 없기 때문에, 각 세대가 정확히 한번씩만 등장하는 이 문제에서 적합하다.

📌 코드 한 줄씩 상세히 복기하기 

🔍 WITH RECURSIVE 재귀함수 정의

  • 2~4번째 줄 (Anchor Member)
    parent_id가 NULL인 개체들은 최초의 개체, 즉 1세대임을 의미한다. 최초 개체의 generation을 1로 지정하고, 이들을 재귀의 기준(Anchor member)으로 삼는다.
  • 5번째 줄 (UNION ALL)
    Anchor에서 나온 1세대 결과와 아래에서 계속 찾을 자식 세대들을 모두 합치기 위해 사용한다.
  • 6~8번째 줄 (Recursive Member)
    이미 찾은 세대의 결과(generations)와 원본 데이터를 JOIN 하면서 다음 세대를 찾아가는 과정이다. JOIN 조건은 원본 테이블의 child.parent_id와 이미 찾은 부모 세대의 parent.id가 같아야 한다. 즉, “부모 세대와 자식 세대를 연결”해가는 과정이며, 세대를 하나씩 증가(parent.generation + 1)시키며 확장해간다.

이렇게 Anchor member(최초 세대)와 Recursive member(이후 모든 세대)가 계속 반복되면서, 전체 세대를 모두 찾아 generations 라는 임시 테이블이 만들어진다.


🔍 결과 조회 및 NOT EXISTS로 자식 없는(leaf) 개체 필터링

  • 9번째 줄 (재귀 결과 활용)
    재귀 호출로 완성된 전체 세대 데이터를 이제 실제 필요한 형식으로 가공하여 출력한다.
  • 10~11번째 줄 (NOT EXISTS)
    이 문제에서는 “자식이 없는 개체”를 찾는 게 핵심이다.
    여기서 NOT EXISTS는 하위 쿼리 결과가 하나도 없는 경우에만 참(True)을 반환한다. 즉,
SELECT 1 FROM ECOLI_DATA AS e2 WHERE e2.parent_id = g.id
위 서브쿼리가 의미하는 건, 현재 개체(g.id)를 부모로 가진 자식 개체가 존재하는지를 검사하는 것이다.

 

g.id e2.parent_id 존재 여부 EXISTS 결과 NOT EXISTS 결과
1 X (자식 없음) FALSE TRUE
2 O (자식 있음: 3,4,5) TRUE FALSE ❌
3 X (자식 없음) FALSE TRUE
4 O (자식 있음: 6,7) TRUE FALSE ❌
5 X (자식 없음) FALSE TRUE
6 O (자식 있음: 8) TRUE FALSE ❌
7 X (자식 없음) FALSE TRUE
8 X (자식 없음) FALSE TRUE

위 표를 보면 명확히 알 수 있듯, NOT EXISTS를 통해 자식이 없는(leaf) 개체만 걸러서 선택된다.


🔍 최종 집계와 정렬

  • 위에서 걸러낸 자식이 없는 개체들을 세대별로(GROUP BY g.generation) 몇 개인지 카운트하고(COUNT(*)),
  • 세대를 오름차순으로(ORDER BY g.generation) 정렬하여 최종 결과를 얻는다.

📌 언제 이 방법을 써야 하나?

데이터베이스 테이블이 자기 자신을 참조하는 계층 구조를 가질 때, 특히 깊이를 알 수 없는 경우에 자주 사용한다.

대표적 예시:

  • 회사의 조직도(CEO → 부장 → 과장 → 사원)
  • 게시글의 댓글과 답글 관계
  • 대장균처럼 분화되어 계속 이어지는 세대 관계(이 문제와 동일한 경우)

이러한 상황에서 WITH RECURSIVE + UNION ALL + NOT EXISTS를 사용하면 효율적이고 직관적인 SQL 쿼리를 작성할 수 있다.


결론적으로 다시 요약하면

  • 계층 구조 데이터를 다룰 때 → WITH RECURSIVE를 사용.
  • 최초 기준(Anchor)을 세우고, 식 세대(Recursive)를 찾아 내려가며 반복 확장.
  • 두 결과를 UNION ALL로 합쳐 모든 세대를 동적 생성.
  • 최종적으로 자식 없는 leaf 개체는 NOT EXISTS로 필터링하여 최종 결과를 도출.

문제 설명

대장균들은 일정 주기로 분화하며, 분화를 시작한 개체를 부모 개체, 분화가 되어 나온 개체를 자식 개체라고 합니다. 다음은 실험실에서 배양한 대장균들의 정보를 담은 ECOLI_DATA 테이블입니다. ECOLI_DATA 테이블의 구조는 다음과 같으며, ID, PARENT_ID, SIZE_OF_COLONY, DIFFERENTIATION_DATE, GENOTYPE 은 각각 대장균 개체의 ID, 부모 개체의 ID, 개체의 크기, 분화되어 나온 날짜, 개체의 형질을 나타냅니다.

최초의 대장균 개체의 PARENT_ID 는 NULL 값입니다.

문제

부모의 형질을 모두 보유한 대장균의 ID(ID), 대장균의 형질(GENOTYPE), 부모 대장균의 형질(PARENT_GENOTYPE)을 출력하는 SQL 문을 작성해주세요. 이때 결과는 ID에 대해 오름차순 정렬해주세요.

 

예시

예를 들어 ECOLI_DATA 테이블이 다음과 같다면

ID PARENT_ID SIZE_OF_COLONY DIFFERNETIATION_DATE GENOTYPE
1 NULL 10 2019/01/01 1
2 1 2 2019/01/01 1
3 1 100 2020/01/01 3
4 2 16 2020/01/01 2
5 4 17 2021/01/01 8
6 3 101 2021/01/01 5
7 2 101 2022/01/01 5
8 6 1 2022/01/01 13

각 대장균 별 형질을 2진수로 나타내면 다음과 같습다.

ID 1 : 1₍₂₎

ID 2 : 1₍₂₎

ID 3 : 11₍₂₎

ID 4 : 10₍₂₎

ID 5 : 1000₍₂₎

ID 6 : 101₍₂₎

ID 7 : 101₍₂₎

ID 8 : 1101₍₂₎

각 대장균 별 보유한 형질을 다음과 같습니다.

ID 1 : 1

ID 2 : 1

ID 3 : 1, 2

ID 4 : 2

ID 5 : 4

ID 6 : 1, 3

ID 7 : 1, 3

ID 8 : 1, 3, 4

각 개체별로 살펴보면 다음과 같습니다.

ID 1 : 최초의 대장균 개체이므로 부모가 없습니다.

ID 2 : 부모는 ID 1 이며 부모의 형질인 1번 형질을 보유하고 있습니다.

ID 3 : 부모는 ID 1 이며 부모의 형질인 1번 형질을 보유하고 있습니다.

ID 4 : 부모는 ID 2 이며 부모의 형질인 1번 형질을 보유하고 있지 않습니다.

ID 5 : 부모는 ID 4 이며 부모의 형질인 2번 형질을 보유하고 있지 않습니다.

ID 6 : 부모는 ID 3 이며 부모의 형질 1, 2번 중 2 번 형질을 보유하고 있지 않습니다.

ID 7 : 부모는 ID 2 이며 부모의 형질인 1번 형질을 보유하고 있습니다.

ID 8 : 부모는 ID 6 이며 부모의 형질 1, 3번을 모두 보유하고 있습니다.

따라서 부모의 형질을 모두 보유한 개체는 ID 2, ID 3, ID 7, ID 8 이다.


위 문제를 풀기 위해서는 먼저 자식 ID와 부모 ID의 형질의 비교가 필요하기 때문에 자기 자신의 TABLE을 JOIN 하는 것이 필요하다.

from ECOLI_DATA C join ECOLI_DATA P on C.PARENT_ID = P.ID

이런 식으로 JOIN을 하게 되면 어떻게 되는지 표를 통해 확인해보면 아래와 같다.

이를 해석하면, C와 P라는 두 개의 별칭을 사용해 ECOLI_DATA TABLE을 두 번 불러온 형태이다.

이 때, C.PARENT_ID = P.ID 조건을 통해, 자식 TABLE(C)의 PARENT_ID와 부모 TABLE(P)의 ID가 같은 행을 연결(JOIN)한다.

즉, 같은 TABLE이라도 현재 레코드(C) 입장에서 그 부모 레코드(P)가 어떤 것인지 찾아내기 위해 동일 TABLE과의 JOIN이 필요한 것이다. 이렇게 자식이 어떤 부모 ID를 가지고 있는 지 맵핑한 후, 문제에서 원하는 것을 찾기 위해 비트 연산자를 사용한다.

예시로 자식 ID 3번을 보면, 부모 ID가 1번이고 개체의 형질이 3 이다. ID 1번 개체의 형질은 1이고 3과 1을 이진수로 표현하면,

3 -> 011

1 -> 001

이다. 이를 비교해서 자식이 부모의 형질을 보유하는 지 알아볼 수 있는 방법은 바로 & 연산이다.

011 & 001 -> 001

& 연산을 했을 때 부모 형질을 보유한다면 결과값은 부모의 형질이 나오게 된다.

 

이러한 사실들을 근거로 최종적으로 코드를 작성한다면 아래와 같다.

select C.ID, C.GENOTYPE, P.GENOTYPE as PARENT_GENOTYPE
from ECOLI_DATA C join ECOLI_DATA P on C.PARENT_ID = P.ID
where (C.GENOTYPE & P.GENOTYPE = P.GENOTYPE)
order by C.ID

'SQL' 카테고리의 다른 글

[MySQL] 멸종위기의 대장균 찾기  (0) 2025.04.30
[MySQL] 특정 형질을 가지는 대장균 찾기  (0) 2025.04.08

문제 설명

대장균들은 일정 주기로 분화하며, 분화를 시작한 개체를 부모 개체, 분화가 되어 나온 개체를 자식 개체라고 합니다. 다음은 실험실에서 배양한 대장균들의 정보를 담은 ECOLI_DATA 테이블입니다. ECOLI_DATA 테이블의 구조는 다음과 같으며, ID, PARENT_ID, SIZE_OF_COLONY, DIFFERENTIATION_DATE, GENOTYPE 은 각각 대장균 개체의 ID, 부모 개체의 ID, 개체의 크기, 분화되어 나온 날짜, 개체의 형질을 나타냅니다.

Column name Type Nullable
ID INTEGER FALSE
PARENT_ID INTEGER TRUE
SIZE_OF_COLONY INTEGER FALSE
DIFFERENTIATION_DATE DATE FALSE
GENOTYPE INTEGER FALSE

최초의 대장균 개체의 PARENT_ID 는 NULL 값입니다.


문제

2번 형질을 보유하지 않으면서 1번이나 3번 형질을 보유하고 있는 대장균 개체의 수(COUNT)를 출력하는 SQL 문을 작성해주세요. 1번과 3번 형질을 모두 보유하고 있는 경우도 1번이나 3번 형질을 보유하고 있는 경우에 포함합니다.


예시

예를 들어 ECOLI_DATA 테이블이 다음과 같다면

ID PARENT_ID SIZE_OF_COLONY DIFFERENTIATION_DATE GENOTYPE
1 NULL 10 2019/01/01 8
2 NULL 2 2019/01/01 15
3 2 100 2020/01/01 1
4 2 16 2020/01/01 13

 

각 대장균 별 형질을 2진수로 나타내면 다음과 같습니다. 

ID 1 : 1000₍₂₎

ID 2 : 1111₍₂₎

ID 3 : 1₍₂₎

ID 4 : 1101₍₂₎

각 대장균 별 보유한 형질을 다음과 같습니다.

ID 1 : 4

ID 2 : 1, 2, 3, 4

ID 3 : 1

ID 4 : 1, 3, 4

따라서 2번 형질이 없는 대장균 개체는 ID 1, ID 3, ID 4 이며 이 중 1번이나 3번 형질을 보유한 대장균 개체는 ID 3, ID 4 입니다.

따라서 결과는 다음과 같아야 합니다.

COUNT
2

문제를 해석하자면, GENOTYPE 컬럼에 있는 값들을 2진수로 나타냈을 때, 각 비트 번호를 형질이라 표현한다.

즉, 8이라는 숫자를 이진수로 표현하면,

1000₍₂₎ 이 되고, 맨 오른쪽을 1번으로 칭했을 때, 4번 형질을 보유하고 있다 얘기하는 것이다.

이 문제를 풀기 위해서는 SQL의 비트 연산자를 알아야 한다.

비트 연산자(AND, OR, NOT)의 개념과 예시

1. 비트 AND(&)

  • 정수들을 이진수로 변환한 뒤, 각 자릿수별로 AND 논리연산(둘 다 1 이면 1, 아니면 0)을 수행
  • 예:
    • 6 & 3을 해석하면
      • 6 → 110₍₂₎
      • 3 → 011₍₂₎ 
      • 110 & 011
        -------
           010₍₂₎ = 2
    • 결과는 2

2. 비트 OR(|)

  • 정수들을 이진수로 변환한 뒤, 각 자릿수별로 OR 논리연산(둘 다 1이면 0, 둘 다 0이면 1, 둘 중 하나가 1이면 1)을 수행
  • 예:
    • 6  | 3을 해석하면
      • 6 → 110₍₂₎
      • 3 → 011₍₂₎ 
        110 | 011
        -----
        111₍₂₎ = 7
      •  
    • 결과는 7

3. 비트 XOR(ˆ)

  • 정수들을 이진수로 변환한 뒤, 각 자릿수별로 XOR 논리연산(둘 중 하나가 1이면 1, 둘 다 0이면 0)을 수행
  • 예:
    • 6 ^ 3을 해석하면
      • 6 → 110₍₂₎
      • 3 → 011₍₂₎
        110 ^ 011
        -----

        101₍₂₎ = 5
    • 결과는 5

4. 비트 NOT(~)

  • 하나의 정수를 이진수로 변환한 뒤, 각 비트를 반전(1이면 0, 0이면 1)시킨 정수를 반환
  • 예:
    • 6(10진수) = 110(2진수)이지만, 실제로는 DB 내부에서 정수는 32비트 혹은 64비트로 표현됨
    • 예를 들어 32비트 정수 기준, 6은 00000000 00000000 00000000 00000110
      • NOT을 하면 11111111 11111111 11111111 11111001 (10진수로 -7에 대응)
    • 따라서 결과는 -7(2의 보수 표현)과 같이 음수가 될 수도 있다.

그 외 Shift 연산자(<<, >>) 가 있지만, SQL에서는 낮은 빈도로 사용

x << n : x의 이진수를 왼쪽으로 n 비트 이동

x >> n : x의 이진수를 오른쪽으로 n 비트 이동

6 << 1 = 110 -> 1100

6 >> 1 = 110 -> 011

계산 방법으로는 x * 2의 n제곱, x / 2의 n제곱

6 << 1 = 6 * 2의 1제곱 = 12

6 >> 1 = 6 / 2의 1제곱 = 3

 

이를 통해, 위 문제를 풀면

select count( * ) as COUNT
from ECOLI_DATA
where !(GENOTYPE & 2)
and ((GENOTYPE & 1) or (GENOTYPE & 4))

GENOTYPE & 2: 비트 번호 2번 (오른쪽에서 2번째 비트 번호)가 0이다. 즉, 2번 형질을 보유하지 않는다.

GENOTYPE & 1 or GENOTYPE & 4: 1번과 3번 형질을 보유한다.

주의!
왜 GENOTYPE & 3이 아니라 4인가?
문제에서 예시로 든 대장균들의 GENOTYPE 값을 2진수로 나타낸 예시를 보면,

1번 형질을 갖고 있다 → 2진수로 ...0001 → 10진수로 1
2번 형질을 갖고 있다 → 2진수로 ...0010 → 10진수로 2
3번 형질을 갖고 있다 → 2진수로 ...0100 → 10진수로 4
4번 형질을 갖고 있다 → 2진수로 ...1000 → 10진수로 8

즉, n번 형질은 **2^(n-1)**에 해당하는 비트가 켜져 있는지(1인지) 여부로 판단

1번 형질 = 2^(1-1) = 2^0 = 1
2번 형질 = 2^(2-1) = 2^1 = 2
3번 형질 = 2^(3-1) = 2^2 = 4
4번 형질 = 2^(4-1) = 2^3 = 8 ... 이런 식으로, 형질 번호에 따라 비트 값이 결정

따라서 3번 형질이 있는지 확인하려면 GENOTYPE & 4가 0이 아닌지 체크하면 된다.

+ Recent posts