2025-08-20 22:32

데이터베이스의 숨겨진 칼, 서브쿼리(Subquery) 완벽 정복 핸드북

데이터를 다루다 보면, 하나의 쿼리만으로는 원하는 결과를 얻기 어려운 복잡한 상황에 부딪히곤 합니다. 마치 여러 개의 재료를 순서대로 넣어야 맛있는 요리가 완성되듯, 데이터 조회 역시 단계적인 접근이 필요할 때가 있죠. 이때, SQL의 강력한 기능인 서브쿼리(Subquery), 즉 하위 쿼리가 그 해답이 될 수 있습니다.

서브쿼리는 다른 SQL 쿼리 내부에 포함된 또 다른 SELECT 문으로, ‘쿼리 안의 쿼리’라고 생각하면 쉽습니다. 복잡한 조건을 걸거나, 기존 테이블에 없는 값을 기준으로 데이터를 필터링하고 싶을 때 마치 숨겨진 칼처럼 날카롭고 유연하게 문제를 해결해 줍니다.

이 핸드북에서는 서브쿼리가 왜 탄생했는지부터 시작하여 그 구조와 종류, 그리고 실전에서 어떻게 활용할 수 있는지까지, 서브쿼리의 모든 것을 깊이 있게 파헤쳐 보겠습니다. 이제 막 SQL에 입문한 초심자부터 더 효율적인 쿼리 작성을 고민하는 중급자까지, 모두에게 훌륭한 안내서가 될 것입니다.

1. 서브쿼리는 왜 만들어졌을까? (탄생 배경)

초기 데이터베이스 시스템은 단순한 데이터 조회와 필터링에 초점을 맞췄습니다. 하지만 데이터가 점점 복잡해지고, 사용자의 요구사항이 다양해지면서 한계에 부딪혔습니다. 예를 들어, ‘평균 급여보다 더 많은 급여를 받는 모든 직원을 찾아라’와 같은 요구사항을 처리하려면 어떻게 해야 할까요?

이 문제를 해결하기 위해서는 두 가지 단계가 필요합니다.

  1. 1단계: 먼저 모든 직원의 ‘평균 급여’를 계산해야 합니다.

  2. 2단계: 계산된 ‘평균 급여’를 기준으로, 이보다 높은 급여를 받는 직원들을 찾아야 합니다.

서브쿼리가 없던 시절에는 이 두 단계를 별개의 쿼리로 실행하고, 첫 번째 쿼리 결과를 두 번째 쿼리에 직접 입력해야 했습니다.

-- 1단계: 평균 급여 계산
SELECT AVG(salary) FROM employees;
-- 결과가 5800이라고 가정
 
-- 2단계: 계산된 값을 이용해 직원 조회
SELECT name, salary FROM employees WHERE salary > 5800;

이 방식은 번거로울 뿐만 아니라, 데이터가 변경될 때마다 평균 급여를 다시 계산해서 쿼리를 수정해야 하는 치명적인 단점이 있었습니다. 데이터의 동적인 변화에 대응할 수 없었던 것이죠.

이러한 비효율과 한계를 극복하기 위해 서브쿼리가 탄생했습니다. 서브쿼리는 이 두 단계를 하나의 논리적인 쿼리로 통합하여, 쿼리가 실행되는 시점에 동적으로 계산된 결과를 즉시 다음 조건에 활용할 수 있게 해줍니다.

-- 서브쿼리를 사용한 통합 쿼리
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

이처럼 서브쿼리는 여러 단계의 데이터 처리 과정을 하나의 SQL 문으로 묶어, 쿼리의 가독성을 높이고, 유지보수를 용이하게 하며, 데이터의 동적인 변화에 유연하게 대응하기 위해 만들어졌습니다. 복잡한 비즈니스 로직을 데이터베이스 단에서 효율적으로 처리할 수 있는 강력한 무기가 탄생한 것입니다.

2. 서브쿼리의 구조와 종류: 어디에, 어떻게 사용할까?

서브쿼리는 그 위치와 반환하는 값의 형태에 따라 크게 세 가지로 나눌 수 있습니다. 각각의 쓰임새가 다르므로 그 특징을 명확히 이해하는 것이 중요합니다.

A. 스칼라 서브쿼리 (Scalar Subquery)

  • 정의: 단 하나의 값(하나의 행, 하나의 열)을 반환하는 서브쿼리입니다.

  • 비유: 하나의 숫자나 문자열 같은 ‘단일 값’으로 치환될 수 있는 변수와 같습니다.

  • 사용 위치: SELECT 절, WHERE 절, HAVING 절 등 단일 값이 들어갈 수 있는 대부분의 위치에서 사용할 수 있습니다.

예시: 각 직원의 급여가 전체 평균 급여와 얼마나 차이 나는지 계산하기

SELECT
    name,
    salary,
    salary - (SELECT AVG(salary) FROM employees) AS salary_difference
FROM
    employees;

여기서 (SELECT AVG(salary) FROM employees) 부분이 스칼라 서브쿼리입니다. 이 쿼리는 실행 시점에 전체 직원의 평균 급여라는 단 하나의 숫자 값으로 계산되어, 외부 쿼리의 모든 행에 대해 동일하게 적용됩니다.

B. 인라인 뷰 (Inline View)

  • 정의: 테이블 형태의 결과(여러 행, 여러 열)를 반환하는 서브쿼리입니다.

  • 비유: 쿼리가 실행되는 동안에만 존재하는 ‘임시 테이블’ 또는 ‘가상 테이블’과 같습니다.

  • 사용 위치: 주로 FROM 절에서 사용됩니다. FROM 절에 테이블 이름 대신 서브쿼리를 사용하여, 마치 하나의 테이블처럼 다룰 수 있습니다.

예시: 각 부서별 평균 급여를 계산하고, 이 결과를 ‘departments’ 테이블과 조인하여 부서 이름과 함께 출력하기

SELECT
    d.department_name,
    e_avg.avg_salary
FROM
    departments d
JOIN
    (SELECT department_id, AVG(salary) AS avg_salary
     FROM employees
     GROUP BY department_id) AS e_avg
ON
    d.department_id = e_avg.department_id;

FROM 절 안에 있는 (SELECT ...) 부분이 인라인 뷰입니다. 이 서브쿼리는 department_idavg_salary라는 두 개의 열을 가진 테이블 형태의 결과를 반환합니다. 외부 쿼리는 이 임시 테이블에 e_avg라는 별칭(alias)을 붙여 실제 테이블처럼 JOIN 연산을 수행합니다. 복잡한 집계 데이터를 미리 계산하고, 다른 테이블과 연결할 때 매우 유용합니다.

C. 중첩 서브쿼리 (Nested Subquery)

  • 정의: 주로 WHERE 절에서 다른 연산자(IN, NOT IN, EXISTS, ANY, ALL 등)와 함께 사용되어, 조건절의 일부로 작동하는 서브쿼리입니다.
  • 비유: 특정 조건을 만족하는 ‘값들의 목록’이나 ‘존재 여부’를 판단하는 필터 역할을 합니다.
  • 사용 위치: WHERE 절, HAVING 절에서 조건 비교를 위해 사용됩니다.

1) 다중 행 서브쿼리 (Multi-row Subquery) - IN, ANY, ALL

여러 개의 행을 반환하는 서브쿼리로, IN 연산자와 함께 가장 많이 쓰입니다.

예시: ‘IT’ 부서 또는 ‘Sales’ 부서에 속한 모든 직원의 정보 조회하기

SELECT name, department_id
FROM employees
WHERE department_id IN (SELECT department_id
                        FROM departments
                        WHERE department_name IN ('IT', 'Sales'));

WHERE절의 서브쿼리는 'IT'와 'Sales' 부서의department_id목록(예: 10, 20)을 반환합니다. 외부 쿼리는employees테이블에서department_id`가 이 목록에 포함된 직원들을 필터링합니다.

2) 연관 서브쿼리 (Correlated Subquery) - EXISTS

외부 쿼리의 컬럼을 참조하는 서브쿼리입니다. 외부 쿼리의 각 행이 처리될 때마다 서브쿼리가 반복 실행되므로 성능에 영향을 줄 수 있어 주의가 필요합니다.

예시: 최소 한 명 이상의 직원을 가진 부서의 정보만 조회하기

SELECT department_name
FROM departments d
WHERE EXISTS (SELECT 1
              FROM employees e
              WHERE e.department_id = d.department_id);

이 쿼리는 departments 테이블의 각 행(부서)을 순회하면서, 해당 부서 ID(d.department_id)를 가진 직원이 employees 테이블에 존재하는지(EXISTS) 확인합니다. 서브쿼리는 조건에 맞는 데이터가 한 건이라도 있으면 TRUE를 반환하고, 외부 쿼리는 해당 부서 정보를 결과에 포함시킵니다. EXISTS는 실제 데이터를 가져오는 것이 아니라 존재 여부만 확인하므로, SELECT 절에 1이나 * 등 어떤 것을 써도 무방합니다.

3. 실전! 서브쿼리 사용법 및 심화 내용

이론을 알았으니 이제 실전 예제를 통해 서브쿼리를 어떻게 활용할 수 있는지 살펴보겠습니다.

A. UPDATE 문에서 서브쿼리 활용하기

UPDATE 문의 SET 절이나 WHERE 절에서도 서브쿼리를 사용할 수 있습니다.

예시: 모든 직원의 급여를 해당 직원이 속한 부서의 평균 급여로 갱신하기 (연관 서브쿼리 활용)

UPDATE employees e
SET salary = (SELECT AVG(salary)
              FROM employees
              WHERE department_id = e.department_id)
WHERE
    e.department_id IS NOT NULL;

이 쿼리는 employees 테이블의 각 직원(e)에 대해, 해당 직원의 부서 ID(e.department_id)와 동일한 부서에 속한 직원들의 평균 급여를 계산하여 그 값으로 salary를 업데이트합니다. 외부 쿼리의 e를 내부 서브쿼리가 참조하는 연관 서브쿼리의 대표적인 예시입니다.

B. DELETE 문에서 서브쿼리 활용하기

DELETE 문에서는 WHERE 절에 서브쿼리를 사용하여 특정 조건을 만족하는 데이터를 삭제할 수 있습니다.

예시: 실적이 없는 영업 사원(Sales)을 직원 목록에서 삭제하기

DELETE FROM employees
WHERE employee_id IN (SELECT e.employee_id
                      FROM employees e
                      LEFT JOIN sales_records sr ON e.employee_id = sr.employee_id
                      WHERE e.department_id = (SELECT department_id FROM departments WHERE department_name = 'Sales')
                        AND sr.sale_id IS NULL);

이 쿼리는 여러 서브쿼리를 중첩하여 사용합니다.

  1. 가장 안쪽 서브쿼리는 ‘Sales’ 부서의 ID를 찾습니다.

  2. 중간 서브쿼리는 이 부서 ID를 이용해 영업 사원 목록을 가져오고, sales_records 테이블과 LEFT JOIN하여 판매 기록이 없는(sr.sale_id IS NULL) 직원의 employee_id를 찾습니다.

  3. 최종적으로 DELETE 문은 이 employee_id 목록에 포함된 직원들을 삭제합니다.

C. 서브쿼리 vs. 조인(JOIN): 무엇을 선택해야 할까?

많은 경우 서브쿼리로 작성된 쿼리는 JOIN으로도 표현할 수 있습니다. 예를 들어, ‘IT 부서에 속한 직원 찾기’는 두 가지 방식으로 모두 가능합니다.

-- 서브쿼리 사용
SELECT name
FROM employees
WHERE department_id = (SELECT department_id FROM departments WHERE department_name = 'IT');
 
-- JOIN 사용
SELECT e.name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.department_name = 'IT';

선택 기준:

  • 가독성: 때로는 서브쿼리가 쿼리의 의도를 더 명확하게 보여줄 수 있습니다. ‘어떤 조건을 만족하는 집합 안에서 찾는다’는 논리적 흐름이 중요할 때 서브쿼리가 더 직관적입니다.

  • 성능: 일반적으로 JOIN이 서브쿼리보다 성능이 더 좋은 경향이 있습니다. 특히 대용량 데이터를 다룰 때, 데이터베이스 옵티마이저는 JOIN 연산을 더 효율적으로 처리하는 경우가 많습니다. 연관 서브쿼리는 외부 쿼리의 행마다 반복 실행될 수 있어 성능 저하의 주범이 되기도 합니다.

  • 필요성: 집계 함수 결과를 조건으로 사용해야 하는 경우(예: WHERE salary > (SELECT AVG(salary) ...)), 서브쿼리를 사용해야만 합니다. JOIN으로는 동일한 로직을 한 번에 구현하기 어렵습니다.

결론적으로, 정답은 없습니다. 쿼리의 복잡성, 데이터의 크기, 그리고 팀의 코딩 컨벤션을 고려하여 더 읽기 쉽고 유지보수하기 좋으며, 성능적으로 효율적인 방법을 선택하는 것이 현명합니다.

4. 핸드북을 마치며

서브쿼리는 단순히 쿼리를 중첩시키는 기술을 넘어, 복잡한 데이터 요구사항을 논리적이고 구조적으로 해결할 수 있게 해주는 강력한 도구입니다. 스칼라 서브쿼리로 동적인 값을 만들고, 인라인 뷰로 임시 테이블을 생성하며, 중첩 서브쿼리로 정교한 필터를 구현하는 방법을 익힘으로써 여러분의 SQL 실력은 한 단계 더 성장할 것입니다.

처음에는 서브쿼리의 구조가 다소 복잡하게 느껴질 수 있습니다. 하지만 이 핸드북에서 다룬 예제들을 하나씩 따라 해보고, 여러분이 실제로 다루는 데이터에 적용해 보세요. “어떤 데이터를 먼저 구하고, 그 결과를 어떻게 다음 단계에서 활용할까?” 라는 질문을 스스로에게 던지는 습관을 들인다면, 어느새 서브쿼리를 자유자재로 사용하는 자신을 발견하게 될 것입니다.

데이터의 세계는 깊고 넓지만, 서브쿼리라는 든든한 무기와 함께라면 그 어떤 복잡한 문제도 해결해 나갈 수 있습니다.

레퍼런스(References)

서브쿼리