'East 지역 팀의 직원을 모두 보여 줘'라는 요청에 SQL을 짰다. 팀은 두 개인데 직원은 한 명만 나온다. 나머지가 갑자기 다른 지역으로 발령 난 것은 아니다. 서브쿼리가 반환한 값의 개수와 바깥쪽 비교 연산자가 맞지 않는다.
서브쿼리는 팀 ID를 몇 개 반환할까?
작은 예제를 먼저 고정하자. East에는 Core와 Data 두 팀이 있고, West에는 Ops 한 팀이 있다. 직원 네 명 가운데 Hana는 Core, Jin과 Mira는 Data, Sol은 Ops 소속이다.
CREATE TABLE teams (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
region TEXT NOT NULL
);
CREATE TABLE staff (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
team_id INTEGER NOT NULL REFERENCES teams(id)
);
INSERT INTO teams (id, name, region) VALUES
(1, 'Core', 'East'), (2, 'Data', 'East'), (3, 'Ops', 'West');
INSERT INTO staff (id, name, team_id) VALUES
(11, 'Hana', 1), (12, 'Jin', 2),
(13, 'Sol', 3), (14, 'Mira', 2);안쪽 쿼리만 실행하면 답은 1, 2다.
SELECT id FROM teams WHERE region = 'East' ORDER BY id;
-- 1
-- 2이 서브쿼리는 괄호 안에서 값 하나가 아니라 값 목록을 만든다. 이 사실을 확인하지 않고 바깥쪽에 =를 붙이면 문제가 시작된다.
=를 쓰면 왜 직원 한 명만 나올까?
SELECT name
FROM staff
WHERE team_id = (
SELECT id FROM teams WHERE region = 'East' ORDER BY id
)
ORDER BY id;
-- Hana이 결과는 SQLite에서 실행한 결과다. SQLite는 이처럼 값을 하나 기대하는 자리에 서브쿼리가 여러 행을 돌려주면 첫 행의 첫 값을 사용한다. ORDER BY id를 넣었으므로 여기서는 팀 ID 1만 비교되고 Hana만 남는다. SQLite의 서브쿼리 표현식 설명이 이 동작을 명시한다. 다른 DBMS의 다중 행 처리까지 같은 것으로 단정해서는 안 된다.
ORDER BY는 예제에서 어느 팀이 선택되는지 분명히 해줄 뿐이다. 팀 하나만 가져오라는 요구가 아닌데 LIMIT 1을 더하는 것은 해결이 아니다. 결과가 조용히 한 명으로 줄어드는 쪽이 오히려 디버깅하기 곤란하다.
IN으로 East 팀 두 개를 모두 비교하려면?
도식에서 서브쿼리는 팀 ID 두 개를 반환한다. IN은 두 값을 모두 비교 대상으로 받아 세 직원을 남기므로, 같은 데이터를 쿼리로 확인해 보자.
SELECT name
FROM staff
WHERE team_id IN (
SELECT id FROM teams WHERE region = 'East'
)
ORDER BY id;
-- Hana
-- Jin
-- MiraIN은 team_id가 안쪽 결과 1 또는 2에 속하는지 묻는다. Data의 직원 두 명이 돌아왔다. 안쪽 결과에 팀 ID 1이 중복되어도 IN 자체가 Hana를 두 번 늘리지는 않는다. 목록에 속하는지를 판정하는 조건이기 때문이다.
=와 IN은 언제 선택할까?
조건의 의미부터 정하면 된다.
- 정확히 한 팀 ID를 찾는 조건이라면
=를 쓴다. 그 값이 하나라고 믿는 근거가 고유 키나 유일성 제약인지 확인하자. - 여러 팀 ID가 가능한 조건이라면
IN을 쓴다. 이 예제의region = 'East'는 한 지역에 팀이 여럿 있을 수 있으므로 여기에 해당한다.
아무 팀도 없는 North로 조건을 바꾸면 이 데이터에서 두 쿼리 모두 직원 0명을 반환한다. 다만 0건과 여러 건이 같은 설계 문제는 아니다. 요구사항이 '팀 하나'라면 결과가 0건·2건이 되는 경우를 별도로 처리해야 하고, '그 지역의 모든 팀'이라면 여러 건이 정상이다. 빈 결과를 숨기려고 LIMIT 1을 붙이기 전에, 안쪽 쿼리의 반환 행 수부터 확인하자.
핵심 요약
서브쿼리의 괄호보다 중요한 것은 몇 행을 돌려주는지다. East 팀은 두 개다. =는 한 값 비교 자리에 이 목록을 넣어 SQLite에서 Hana만 남겼고, IN은 두 ID를 모두 비교해 세 직원을 찾았다. 예상보다 적게 나오면 안쪽 쿼리를 단독 실행해 행 수를 확인하고, 한 값을 기대한 것인지 여러 값을 허용한 것인지부터 맞추자.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

