본문으로 바로가기
TaeyoungKim.dev

SQL 서브쿼리 =와 IN 차이: 여러 행일 때 조회 결과가 빠지는 이유

DB/SQL작성 약 3분 읽기TaeyoungKim
LinkedInX

'East 지역 팀의 직원을 모두 보여 줘'라는 요청에 SQL을 짰다. 팀은 두 개인데 직원은 한 명만 나온다. 나머지가 갑자기 다른 지역으로 발령 난 것은 아니다. 서브쿼리가 반환한 값의 개수와 바깥쪽 비교 연산자가 맞지 않는다.

서브쿼리는 팀 ID를 몇 개 반환할까?

작은 예제를 먼저 고정하자. East에는 Core와 Data 두 팀이 있고, West에는 Ops 한 팀이 있다. 직원 네 명 가운데 Hana는 Core, Jin과 Mira는 Data, Sol은 Ops 소속이다.

sql
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다.

sql
SELECT id FROM teams WHERE region = 'East' ORDER BY id;
-- 1
-- 2

이 서브쿼리는 괄호 안에서 값 하나가 아니라 값 목록을 만든다. 이 사실을 확인하지 않고 바깥쪽에 =를 붙이면 문제가 시작된다.

=를 쓰면 왜 직원 한 명만 나올까?

sql
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은 두 값을 모두 비교 대상으로 받아 세 직원을 남기므로, 같은 데이터를 쿼리로 확인해 보자.

sql
SELECT name
FROM staff
WHERE team_id IN (
  SELECT id FROM teams WHERE region = 'East'
)
ORDER BY id;
-- Hana
-- Jin
-- Mira

IN은 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를 모두 비교해 세 직원을 찾았다. 예상보다 적게 나오면 안쪽 쿼리를 단독 실행해 행 수를 확인하고, 한 값을 기대한 것인지 여러 값을 허용한 것인지부터 맞추자.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

#SQL#SQL 서브쿼리#SQL IN#단일 행 서브쿼리#다중 행 서브쿼리

함께 읽으면 좋은 글