문제 설명
어느 한 게임에서 사용되는 아이템들은 업그레이드가 가능합니다.
'ITEM_A'->'ITEM_B'와 같이 업그레이드가 가능할 때
'ITEM_A'를 'ITEM_B'의 PARENT 아이템,
PARENT 아이템이 없는 아이템을 ROOT 아이템이라고 합니다.
예를 들어 'ITEM_A'->'ITEM_B'->'ITEM_C' 와 같이 업그레이드가 가능한 아이템이 있다면
'ITEM_C'의 PARENT 아이템은 'ITEM_B'
'ITEM_B'의 PARENT 아이템은 'ITEM_A'
ROOT 아이템은 'ITEM_A'가 됩니다.
다음은 해당 게임에서 사용되는 아이템 정보를 담은
ITEM_INFO
테이블과 아이템 관계를 나타낸 ITEM_TREE
테이블입니다.ITEM_INFO
테이블은 다음과 같으며, ITEM_ID
, ITEM_NAME
, RARITY
, PRICE
는 각각 아이템 ID, 아이템 명, 아이템의 희귀도, 아이템의 가격을 나타냅니다.Column name | Type | Nullable |
ITEM_ID | INTEGER | FALSE |
ITEM_NAME | VARCHAR(N) | FALSE |
RARITY | INTEGER | FALSE |
PRICE | INTEGER | FALSE |
ITEM_TREE
테이블은 다음과 같으며, ITEM_ID
, PARENT_ITEM_ID
는 각각 아이템 ID, PARENT 아이템의 ID를 나타냅니다.Column name | Type | Nullable |
ITEM_ID | INTEGER | FALSE |
PARENT_ITEM_ID | INTEGER | TRUE |
단, 각 아이템들은 오직 하나의 PARENT 아이템 ID 를 가지며, ROOT 아이템의 PARENT 아이템 ID 는 NULL 입니다.
ROOT 아이템이 없는 경우는 존재하지 않습니다.
예시
예를 들어
ITEM_INFO
테이블이 다음과 같고ITEM_ID | ITEM_NAME | RARITY | PRICE |
0 | ITEM_A | RARE | 10000 |
1 | ITEM_B | RARE | 9000 |
2 | ITEM_C | LEGEND | 11000 |
3 | ITEM_D | RARE | 10000 |
4 | ITEM_E | RARE | 12000 |
ITEM_TREE
테이블이 다음과 같다면ITEM_ID | PARENT_ITEM_ID |
0 | NULL |
1 | 0 |
2 | 0 |
3 | 1 |
4 | 1 |
'ITEM_A' 는 'ITEM_B', 'ITEM_C' 로 업그레이드가 가능하며 'ITEM_B' 는 'ITEM_D', 'ITEM_E' 로 업그레이드가 가능합니다. 'ITEM_C', 'ITEM_D', 'ITEM_E' 는 더 이상 업그레이드가 가능하지 않으므로 결과는 다음과 같이 나와야 합니다.
ITEM_ID | ITEM_NAME | RARITY |
4 | ITEM_E | RARE |
3 | ITEM_D | RARE |
2 | ITEM_C | LEGEND |
코드
SELECT
ITEM_ID,
ITEM_NAME,
RARITY
FROM
ITEM_INFO II
WHERE
NOT EXISTS
(
SELECT
1
FROM
ITEM_TREE IT
WHERE
II.ITEM_ID = IT.PARENT_ITEM_ID
)
ORDER BY
ITEM_ID DESC;

SQL의 스코프(Scope) 규칙
이러한 접근이 가능한 것은 SQL의 변수 스코프(Variable Scope) 규칙 덕분입니다.
- 내부 스코프는 외부 스코프에 접근 가능: 서브쿼리(내부 쿼리)는 외부 쿼리(상위 스코프) 내부에 정의되므로, 내부 스코프는 상위 스코프에 정의된 요소(테이블, 칼럼)에 접근할 수 있습니다.
- 테이블 별칭(Alias) 사용: 외부 쿼리의 테이블에 별칭(
Customers c
)을 지정하면, 서브쿼리 내부에서 이 별칭을 사용하여 외부 테이블의 칼럼(c.customer_id
)에 명확하게 접근할 수 있습니다. 이것은 필수적인 프로그래밍 패턴입니다.
- 처리 순서: 데이터베이스 관리 시스템(DBMS)은 쿼리를 실행할 때 외부 쿼리에서 한 행을 선택한 후, 해당 행의 정보를 가지고 서브쿼리를 실행하는 방식으로 처리 순서를 정의합니다.
⚠️ 주의할 점 (FROM 절 접근 불가)
서브쿼리가 외부 쿼리의
FROM
절에 있는 테이블에 접근하는 것은 맞지만, 이는 WHERE
, SELECT
, 또는 HAVING
절에 위치한 서브쿼리에 한해서입니다.FROM
절에 사용되는 서브쿼리(인라인 뷰/Derived Table)는 독립적으로 먼저 실행되어 하나의 임시 테이블을 생성합니다. 따라서FROM
절에 있는 서브쿼리는 외부 쿼리의 테이블에 직접 접근할 수 없습니다. 이 서브쿼리는 일반 서브쿼리(비상관 서브쿼리)의 형태로 동작합니다.
쿼리 코드
SELECT
c.customer_id,
c.name
FROM
Customers c -- 외부 쿼리 (Customers 테이블의 각 행 c에 대해 실행)
WHERE
NOT EXISTS (
SELECT 1 -- 서브쿼리 (존재 여부만 확인하므로 SELECT * 대신 '1'을 사용하는 것이 효율적)
FROM Orders o
WHERE o.customer_id = c.customer_id -- 핵심: 상관 조건 (외부 c.customer_id와 내부 o.customer_id가 일치하는지 확인)
);
(NOT IN) VS (NOT EXISTS)
NOT IN
과 NOT EXISTS
의 차이를 깔끔하고 효율적으로 요약해 드리겠습니다.구분 | NOT IN | NOT EXISTS |
핵심 동작 | 서브쿼리의 전체 결과 집합과 외부 값을 일일이 비교 | 서브쿼리 내에 관련 행이 존재하는지 여부만 확인 (상관 서브쿼리) |
NULL 값 | 서브쿼리 결과에 NULL 이 하나라도 있으면, 결과가 부정확해짐 (대부분의 행을 제외) | NULL 값 문제에서 자유로움 (행 존재 여부만 따짐) |
성능 (대부분) | 서브쿼리 결과를 메모리/임시 테이블에 저장 후 처리 → 대용량에서 상대적으로 느림 | 인덱스를 활용하여 빠른 검색 중단(Short-Circuiting) 가능 → 대부분의 경우 더 빠르고 안정적임 |
권장 사용 | 서브쿼리 결과가 매우 작고 NULL이 없는 것이 확실할 때 (가독성) | 대부분의 경우 (특히 대용량 데이터에서) → 안정성과 성능 우위 |
결론: 안정성과 성능을 위해 **
NOT EXISTS
**를 사용하는 것이 SQL 개발의 모범 사례입니다. 👍Share article