Menu

SQLite 서브쿼리 완벽 정리: WHERE, FROM, SELECT 중첩 활용법

SQLite에서 SELECT 안에 SELECT를 중첩하는 방법을 정리했습니다. 스칼라 서브쿼리, IN/EXISTS, 인라인 뷰, 상관 서브쿼리, 그리고 JOIN이 더 나은 경우까지 한 번에 살펴봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

서브쿼리란? SELECT 안에 들어간 또 다른 SELECT

SQLite 서브쿼리는 말 그대로 SELECT 문 안에 또 다른 SELECT 문을 괄호로 감싸 넣은 것입니다. SQLite는 안쪽 쿼리를 먼저 실행한 뒤, 그 결과를 바깥 쿼리에 넘겨서 사용합니다.

앞으로 계속 써먹을 예제 데이터를 먼저 준비해 두겠습니다:

주문 5건, 고객 4명, 그리고 그중 2명은 아직 아무것도 주문하지 않은 상태입니다. 이 데이터를 앞으로 계속 사용할 거예요.

WHERE 절 서브쿼리: 목록으로 필터링하기

가장 흔하게 쓰는 형태입니다. 안쪽 쿼리에서 id 목록을 뽑아낸 다음, 바깥쪽 쿼리에서 그 목록을 기준으로 걸러내는 방식이죠.

안쪽 쿼리는 orders에 나타나는 모든 customer_id를 뽑아냅니다. 그리고 바깥쪽 쿼리는 그 목록에 id가 포함된 고객만 남기죠. 결과적으로 Cleo, Boris, Ada는 나오지만, 주문이 하나도 없는 Dmitri는 빠집니다.

IN (SELECT ...) 형태는 "B에 매칭되는 행이 있는 A의 행"을 뽑을 때 가장 많이 쓰는 SQLite 서브쿼리 패턴입니다. 머릿속에서는 "이 컬럼의 값이 안쪽 쿼리가 돌려준 값들 중 하나일 때"라고 읽으면 됩니다.

NOT IN: NULL을 조심하세요

반대 질문, 즉 "주문한 적이 없는 고객은 누구인가?"는 한 줄만 바꾸면 됩니다.

여기서는 잘 동작합니다. 그런데 NOT IN에는 함정이 하나 있어요. 서브쿼리 결과에 NULL이 단 하나라도 섞이면 NOT IN 전체가 NULL로 평가되고(즉 TRUE가 아니죠), 결과로 아무 행도 돌아오지 않습니다. 에러도 안 나고 조용히 빈 결과가 나오니 당황하기 딱 좋죠.

NULL이 들어 있을 수 있는 컬럼에 NOT IN을 쓸 때는 다음 습관을 들이는 게 안전합니다:

아니면 NOT EXISTS를 쓰면 이런 문제 자체가 없어요. 이건 뒤에서 다룹니다.

스칼라 서브쿼리: 한 행, 한 컬럼

스칼라 서브쿼리는 단일 값(한 행, 한 컬럼)을 반환하기 때문에, 값이 들어갈 수 있는 자리라면 어디든 쓸 수 있습니다.

안쪽의 SELECT MAX(total) FROM orders200을 반환합니다. 그러면 바깥쪽 쿼리가 이 값과 일치하는 주문만 골라내죠. 집계값과 비교해야 할 때 두루 쓸 수 있는 방식입니다.

스칼라 서브쿼리는 SELECT 절에 넣어서 모든 행에 계산된 값을 함께 붙여 줄 수도 있습니다:

customers의 각 행마다 안쪽 쿼리가 한 번씩 실행되고, 그때마다 customers.id 값이 끼워 넣어집니다. 이게 바로 상관 서브쿼리(correlated subquery) 인데, 자세한 내용은 뒤에서 다시 살펴보겠습니다. 이렇게 "한 행당 숫자 하나"가 필요한 경우라면 보통 LEFT JOINGROUP BY 조합이 성능 면에서 더 낫지만, 스칼라 서브쿼리 형태는 코드 자체가 깔끔하게 잘 읽힌다는 장점이 있죠.

EXISTS: 매칭되는 행이 있는지만 확인하기

EXISTSIN보다 한결 조용한 친척쯤 됩니다. 값이 뭔지는 신경 쓰지 않고, 서브쿼리가 아무 행이라도 반환하는지만 따져보거든요. 그래서 안쪽에는 보통 SELECT 1을 적습니다 — 어차피 컬럼이 뭐가 됐든 의미가 없으니까요.

이 쿼리는 100을 초과하는 주문을 한 번이라도 넣은 고객을 찾아냅니다. 안쪽 쿼리에서 바깥쪽 쿼리의 c.id를 참조하고 있죠. 바로 이 부분 때문에 상관 서브쿼리(correlated subquery)가 됩니다. SQLite는 일치하는 행을 하나라도 찾으면 그 즉시 안쪽 테이블 스캔을 멈추기 때문에, "이 행과 연결된 행이 존재하는가?"를 묻는 상황에서는 EXISTSIN보다 더 빠른 경우가 많습니다.

반대로 "연결된 행이 하나도 없는가?"를 물을 때는 NOT EXISTS를 쓰는 게 NULL 안전한 방법입니다:

FROM 절에 서브쿼리 쓰기: 파생 테이블 만들기

테이블이 올 수 있는 자리라면 어디든 서브쿼리를 넣을 수 있습니다. FROM 절도 예외는 아닙니다. 안쪽 쿼리에 별칭을 붙이면 임시로 만들어진 "파생 테이블(derived table)"이 되고, 이 테이블을 대상으로 다시 조인하거나 필터링하거나 집계할 수 있습니다.

안쪽 쿼리는 고객별 총합을 계산하고, 바깥 쿼리는 그 총합을 국가별로 평균 낸다. 이렇게 두 단계로 집계해야 하는 상황이야말로 파생 테이블(derived table)이 필요한 전형적인 케이스다 — 한 번의 GROUP BY만으로는 처리할 수 없을 때 쓰는 방법이다.

이때 AS per_customer 별칭은 필수다. 모든 파생 테이블에는 반드시 이름을 붙여야 한다.

상관 서브쿼리: 바깥 행마다 실행되는 SQLite 서브쿼리

서브쿼리가 바깥 쿼리의 컬럼을 참조하면 상관 서브쿼리(correlated subquery) 가 된다. 이 경우 SQLite는 바깥 쿼리의 행 하나하나마다 안쪽 쿼리를 다시 실행해야 한다. 유연한 만큼 비용이 꽤 커질 수 있다.

각 고객별로 가장 큰 주문 금액을 찾는 예제입니다. 안쪽 쿼리가 customers.id에 의존하기 때문에 고객 한 명마다 한 번씩 실행됩니다. 주문 이력이 없는 고객은 NULL이 나오는데, 보통은 이게 원하는 동작이죠.

상관 서브쿼리(correlated subquery)는 "A 테이블의 각 행마다 B에서 뭔가를 계산해야 할 때" 가장 자연스러운 선택입니다. 테이블이 작거나 조회 컬럼에 인덱스가 잘 걸려 있다면 충분히 쓸 만합니다. 다만 인덱스가 없는 큰 테이블에서는 배포 전에 꼭 성능을 측정해 보세요. JOIN + GROUP BY 조합이 더 빠를 때가 많습니다.

서브쿼리 vs JOIN, 언제 무엇을 쓸까

아래 두 쿼리는 같은 질문에 대한 답을 구합니다:

두 쿼리는 똑같은 결과를 돌려줍니다. SQLite 옵티마이저는 내부적으로 한 형태를 다른 형태로 자주 다시 써넣기 때문에, 선택 기준은 결국 가독성 입니다.

  • 단순히 행을 걸러내기만 하면 되고, 안쪽 테이블의 컬럼이 결과에 섞여 들어오는 게 싫다면 서브쿼리를 쓰세요.
  • 두 테이블의 컬럼이 결과에 모두 필요하다면 JOIN을 쓰세요.
  • "관련된 행이 하나라도 있나?"를 묻는 거라면 EXISTS가 가장 명확합니다. IN/NOT IN이 가진 NULL 함정도 피할 수 있고요.

판단이 애매할 땐, 소리 내어 읽었을 때 의미가 그대로 전해지는 쪽을 고르면 됩니다.

자주 빠지는 함정: 여러 행을 돌려주는 서브쿼리

=와 함께 쓰는 서브쿼리는 행을 최대 하나만 반환해야 합니다. 그 이상이 나오면 SQLite는 그중 하나를 (사실상 무작위로) 골라버리고, 에러도 없이 조용히 잘못된 결과를 내놓습니다.

안쪽 쿼리가 여러 행을 반환할 수 있다면 IN을 사용하세요:

정확히 한 행만 나와야 한다면, 최소한 결과가 결정적이도록 LIMIT 1ORDER BY를 함께 넣어 주세요. 더 좋은 방법은 데이터 자체로 한 행이 보장되도록 쿼리를 작성하는 것입니다(고유 컬럼으로 필터링).

다음 주제: 공통 테이블 표현식(CTE)

FROM 절 서브쿼리는 금세 다루기 까다로워집니다. 같은 파생 테이블을 두 번 써야 하거나, 중첩이 세 단계까지 깊어질 때가 특히 그렇죠. 공통 테이블 표현식(WITH ... AS (...))을 쓰면 서브쿼리에 미리 이름을 붙여 두고, 쿼리 나머지 부분에서 그 이름으로 참조할 수 있습니다. 다음 페이지에서 자세히 살펴볼게요.

자주 묻는 질문

SQLite에서 서브쿼리란 무엇인가요?

서브쿼리는 다른 SQL 문 안에 괄호로 감싸 넣은 SELECT 문을 말합니다. SQLite는 안쪽 쿼리를 먼저 실행하고 그 결과를 바깥 쿼리에 넘겨줍니다. WHERE, FROM, SELECT 절은 물론 그 외 여러 위치에서도 쓸 수 있습니다.

SQLite의 IN과 EXISTS는 어떤 차이가 있나요?

IN (SELECT ...)은 어떤 값이 서브쿼리가 반환한 행들 중 하나와 일치하는지 확인합니다. 반면 EXISTS (SELECT ...)는 값이 무엇이든 상관없이 서브쿼리가 행을 하나라도 반환하는지만 봅니다. 안쪽 쿼리가 바깥쪽 행을 참조하는 상관 서브쿼리에서는 보통 EXISTS 쪽이 더 자연스럽고 효율적입니다.

서브쿼리와 JOIN 중 어느 쪽을 써야 하나요?

결과에 두 테이블의 컬럼이 모두 필요하면 JOIN이 맞습니다. 단순히 필터링하거나 값 하나만 계산하면 되는 상황이라면 서브쿼리가 깔끔합니다. SQLite 옵티마이저가 내부적으로 두 형태를 서로 변환하는 경우도 많기 때문에, 결국 더 읽기 쉬운 쪽을 고르는 게 정답입니다.

상관 서브쿼리(correlated subquery)란 무엇인가요?

상관 서브쿼리는 바깥쪽 쿼리의 컬럼을 참조하는 서브쿼리로, 바깥 쿼리의 모든 행마다 다시 평가됩니다. 표현력이 좋은 대신 데이터가 많으면 성능이 떨어질 수 있죠. 만약 상관 서브쿼리가 병목 구간으로 잡힌다면 JOIN이나 CTE로 바꿔보면 대부분 해결됩니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기