10.1. 서브쿼리10.1.1. 서브쿼리 개요10.1.2. 사용 목적10.1.3. 서브쿼리의 효율성10.1.4. WITH10.2. 서브쿼리의 종류10.2.1. 스칼라10.2.2 인라인 뷰10.2.3. 중첩10.2.4. IN10.2.5. EXISTS10.2.6. 상관10.2.7. ARRAY
10.1. 서브쿼리
10.1.1. 서브쿼리 개요
서브쿼리는 쿼리 내의 다른 쿼리로 메인쿼리에 필요한 데이터를 필터링, 변환 또는 집계하는 데 사용됩니다.
1개의 SQL문 내에 포함된
SELECT
문을 의미하며, 서브쿼리 밖에 있는 SQL문을 메인쿼리라고 합니다. 서브쿼리는 소괄호 안에 작성되며, 메인쿼리에서 필요한 데이터를 제공하는 데 사용됩니다. 서브쿼리는 단독으로 실행될 수 없으며, 항상 메인쿼리와 연계되어 실행됩니다.
복잡한 데이터 요구사항을 처리하기 위한 효과적인 도구로서, 두 개 이상의 테이블에서 데이터를 참조할 때나 복잡한 SQL 문을 작성할 필요가 있을 때 주로 사용됩니다.

JOIN
으로 작성된 SQL 문은 서브쿼리가 포함된 SQL 문으로 변경할 수 있습니다.
JOIN
도 서브쿼리와 마찬가지로 두 개 이상의 테이블에서 데이터를 가져오는 데 사용하지만, 2개의 테이블에서 1개의 결과를 얻고자 할 때는 서브쿼리문을 사용하고 3개 이상의 테이블에서 데이터를 얻고자 하는 경우는 JOIN
을 사용하는 것을 권장합니다. 3개 이상의 테이블에서는 서브쿼리 대신
JOIN
을 사용할 시 쿼리가 더 간결해지고 가독성이 높아질 수 있기 때문입니다.서브쿼리는
SELECT
, FROM
, WHERE
, HAVING
및 JOIN
절 등 다양한 SQL 절에서 활용될 수 있으며, 메인쿼리보다 먼저 실행되는 종속적인 구조를 가지고 있습니다. 서브쿼리는 소괄호 내에 정의되며, 단일 값 또는 집합 값을 반환할 수 있습니다.
또한, 다양한 비교 연산자(
=
, <
, >
, IN
, NOT IN
, EXIST
, NOT EXIST
등)와 결합하여 복잡한 조건을 표현하는 데 활용할 수 있습니다.서브쿼리의 종류나 목적에 따라서
ORDER BY
절은 사용할 수 없습니다.
조건절 서브쿼리(IN
, EXISTS
등)는 결과 집합의 존재를 확인하는 것이기 때문에 해당 구문 안에서는 ORDER BY
절을 사용할 수 없습니다. 10.1.2. 사용 목적
서브쿼리는 알려지지 않은 기준으로 검색할 때 용이합니다. 테이블 내에서 조건을 설정하기 어려워 다른 테이블에서 조건을 가져와야 할 경우 서브쿼리를 사용합니다. 서브쿼리는 특정 기준값을 미리 계산하고, 그 값을 바탕으로 다른 쿼리에서 활용하는 상황에서 큰 장점을 발휘합니다. 또한 아래와 같이 열 데이터 생성, 임시 테이블 생성, 집합 연산 수행, 데이터 검증, 복잡한 질의처리 시 서브쿼리를 활용할 수 있습니다.
조건 제공: 서브쿼리는
WHERE
나 HAVING
절에서 조건값을 제공하는 데 사용될 수 있습니다. 예를 들어, 특정 평균값 이상의 데이터를 조회할 때, 그 평균값을 구하는 서브쿼리를 사용할 수 있습니다.열 데이터 생성:
SELECT
절에서 서브쿼리를 사용하여 도출된 열 값을 생성할 수 있습니다.임시 테이블 생성:
FROM
절에서 서브쿼리를 사용하여 임시 테이블을 생성하고, 이 테이블을 기반으로 메인 쿼리를 실행할 수 있습니다.집합 연산 수행: 여러 서브쿼리의 결과를
UNION
, INTERSECT
, EXCEPT
등의 연산자로 결합할 수 있습니다.데이터 검증: 서브쿼리를 사용하여 데이터가 특정 조건을 만족하는지 확인하거나, 특정 값을 가지는지 검증할 수 있습니다.
복잡한 질의 처리: 복잡한 데이터 요구사항을 단순화하여 처리하기 위해 여러 단계의 서브쿼리를 중첩해서 사용할 수 있습니다.
10.1.3. 서브쿼리의 효율성
데이터베이스에서 쿼리의 실행 시간은 매우 중요한 지표입니다. 특히 대규모 데이터를 처리하는 경우, 서브쿼리의 비효율적인 사용은 실행 시간의 큰 지연을 초래할 수 있습니다. 따라서 서브쿼리를 사용할 때는 항상 효율성을 고려해야 합니다.
서브쿼리는 다음과 같은 특징을 가지고 있습니다.
코드 재사용: 같은 로직이나 조건을 여러 쿼리에서 사용해야 할 때, 그 로직을 서브쿼리로 만들어 재사용함으로써 코드의 중복을 줄일 수 있습니다.
명시적인 순서 : 서브쿼리를 사용 시, 연산 순서를 명확하게 지정할 수 있습니다.
임시 결과 생성 : 특정 조건에 부합하는 중간 데이터 집합을 생성할 수 있습니다. 이는 메인 쿼리에서 연산을 위한 임시결과로 사용할 수 있습니다.
계층적 질의 : 서브쿼리를 활용하면 데이터 간의 관계를 계층적으로 표현하고 질의할 수 있습니다. 예를 들어, 조직 내의 상하 관계나 가계도처럼 데이터 사이의 계층적 관계를 쉽게 파악하고 조회할 수 있습니다.
복잡성 감소: 서브쿼리를 사용하면 복잡한 로직을 분리하여 메인 쿼리의 가독성을 향상시킬 수 있습니다.
효율성과 관련된 주의 사항
- 너무 많은 중첩된 서브쿼리는 쿼리의 성능을 저하시킬 수 있습니다. 가능한 경우 조인을 사용하여 서브쿼리의 수를 최소화하는 것이 좋습니다.
- 서브쿼리가 불필요하게 많은 데이터를 반환하는 경우, 이는 메인 쿼리의 성능에 부정적인 영향을 미칠 수 있습니다. 서브쿼리에서 반환되는 데이터의 양을 최소화하려면 필터링 조건을 적절히 사용해야 합니다.
- 가능한 경우 서브쿼리에서 조회되는 열에 인덱스를 생성하여 성능을 향상시킬 수 있습니다.
서브쿼리를 활용 시 연산 비용이 추가, 최적화를 받을 수 없어 쿼리가 복잡해질 수 있다는 단점이 있습니다. 따라서 SQL 문에서 서브쿼리를 사용 시, 불필요한
JOIN
연산하지는 않았는지, 테이블 접근을 최소화했는지 고려해야 합니다. 복잡한 쿼리에서 반복적으로 동일한 서브쿼리를 사용하면, 동일한 연산이 여러 번 실행되어 효율성이 떨어질 수 있습니다. 여기서
WITH
절을 사용함으로써 코드의 가독성을 향상시키며, 동일한 서브쿼리를 여러 번 작성하는 것을 피하여 효율성을 높일 수 있습니다.10.1.4. WITH
WITH
절은 쿼리의 시작 부분에서 정의되며, 이후의 주 쿼리에서 여러 번 참조될 수 있는 일시적인 결과 집합을 생성합니다. 쿼리가 실행되는 동안 필요한 컬럼만 추출해 임시 테이블명을 정의하여 불필요한 계산 또는 리소스를 줄일 수 있습니다.WITH
절은 ‘가독성’, ‘재사용성’, ‘로직의 순차적 표현’, ‘효율성’ 이라는 특징을 가지고 있습니다.복잡한 쿼리를 분해하고, 중간 결과를 저장하여 쿼리의 가독성을 향상시키고 구조화하는 데 도움을 줍니다
- 가독성: 복잡한 쿼리의 구조를 명확하게 표현할 수 있으며, 각 부분을 분리하여 읽기 쉽게 만듭니다.
- 재사용성: 동일한 서브쿼리를 여러 번 재사용할 필요 없이 한 번만 정의하면 됩니다.
- 로직의 순차적 표현:
WITH
절을 연쇄적으로 정의하면, 쿼리의 실행 로직을 단계별로 표현할 수 있습니다. 이는 특히 데이터 전처리 단계를 순차적으로 표현할 때 유용합니다.
- 효율성: 데이터베이스에 따라,
WITH
절의 결과를 캐싱하여 주 쿼리에서의 반복된 참조에 대한 연산을 줄일 수 있습니다.
아래 코드와 같이
WITH
절 안에 쿼리를 작성하여 임시 테이블을 생성합니다. 이때, 하나의 테이블뿐만 아니라 콤마(,)를 기준으로 동시에 여러 개의 임시 테이블을 생성할 수 있어 CTE(Common Table Expressions)이라고도 부릅니다.WITH new_table1 AS ( SELECT column1 FROM Table1 ), new_table2 AS ( SELECT column1 FROM Table2 ) SELECT * FROM new_table1;
이전에는 복잡한 쿼리의 경우
FROM
절 안에 SELECT
가 들어가도록 작성하였습니다. SELECT column1 FROM ( SELECT * FROM Table1 );
이때,
WITH
절을 사용하여 복잡한 쿼리를 논리적으로 세분화하고 재사용 가능한 임시 테이블로 생성하여 쿼리를 구조화할 수 있고 가독성을 높일 수 있습니다. 이처럼 복잡한 쿼리를 더 작은 논리적 블록으로 나누어 쉽게 이해하고 유지 보수할 수 있게 됩니다.WITH
절은 적절히 사용하면 많은 이점을 제공하지만, 남발하면 복잡성과 성능 문제를 초래할 수 있습니다. 따라서 WITH
절을 사용할 때는 항상 성능과 가독성의 균형을 고려해야 합니다.예시 1
“employees”의 “employee_id” 값을 조회하는 “employee_data”라는 임시 테이블을 정의하고 “employee_data” 테이블로부터 데이터를 조회합니다.
WITH employee_data AS ( SELECT employee_id FROM `HR.employees` ) SELECT * FROM employee_data;

예시 2
직업별 직원 아이디 수를 “employee_job_counts”라는 임시 테이블로 정의하고 “employee_job_counts” 테이블로부터 데이터를 조회합니다.
WITH employee_job_counts AS ( SELECT job_id , COUNT(employee_id) AS job_count FROM `HR.employees` GROUP BY job_id ) SELECT * FROM employee_job_counts ORDER BY job_count DESC;

예시 3
직종별 직원 수가 20 이상인 직종 코드(”job_code”)와 직원 수를 “employee_counts”라는 임시 테이블을 정의하고 “employee_counts” 테이블로부터 직종별 직원 수를 조회합니다.
WITH employee_counts AS ( SELECT LEFT(job_id,2) AS job_code , COUNT(LEFT(job_id,2)) AS employee_count FROM `HR.employees` GROUP BY job_code HAVING COUNT(job_code)>=20 ) SELECT * FROM employee_counts;

예시 4
국가 테이블과 부서 테이블, 직원 테이블을 이용하여 국가별 부서에 근무하는 직원 수를 구합니다.
WITH dep_loc_join AS ( SELECT d.department_id , d.department_name , l.country_id FROM `HR.departments` AS d LEFT JOIN `HR.locations` AS l ON d.location_id = l.location_id ), dep_loc_emp_join AS ( SELECT e.employee_id , e.department_id , dl.department_name , dl.country_id FROM `HR.employees` AS e LEFT JOIN dep_loc_join AS dl ON e.department_id = dl.department_id ) SELECT country_id , department_name , COUNT(employee_id) AS employee_count FROM dep_loc_emp_join GROUP BY country_id, department_name;
예시 4-1
국가별 어떤 부서들이 있는지 조회하기 위하여 “departments” 테이블과 “locations” 테이블을
LEFT JOIN
하여 “department_id”, “department_name”, “country_id”를 조회하고 “dep_loc_join”라는 임시 테이블을 생성합니다.WITH dep_loc_join AS ( SELECT d.department_id , d.department_name , l.country_id FROM `HR.departments` AS d LEFT JOIN `HR.locations` AS l ON d.location_id = l.location_id ) SELECT * FROM dep_loc_join;

예시 4-2
국가별 부서별 직원 수를 구하기 위해 “employees” 테이블과 예시 4-1의 “dep_loc_join” 테이블을
LEFT JOIN
하여 “employee_id”, “department_id”, “department_name”, “country_id”를 조회하고 “dep_loc_emp_join” 테이블로 생성합니다.WITH dep_loc_join AS ( SELECT d.department_id , d.department_name , l.country_id FROM `HR.departments` AS d LEFT JOIN `HR.locations` AS l ON d.location_id = l.location_id ), dep_loc_emp_join AS ( SELECT e.employee_id , e.department_id , dl.department_name , dl.country_id FROM `HR.employees` AS e LEFT JOIN dep_loc_join AS dl ON e.department_id = dl.department_id ) SELECT * FROM dep_loc_emp_join;

예시 4-3
예시 4-2의 “dep_loc_emp_join” 테이블을 이용하여 국가별 부서별 직원 수를 조회합니다. 이때, “country_id”, “department_name”를 기준으로 묶어준 후 직원 수(”employee_count”)를 구해줍니다.
WITH dep_loc_join AS ( SELECT d.department_id , d.department_name , l.country_id FROM `HR.departments` AS d LEFT JOIN `HR.locations` AS l ON d.location_id = l.location_id ), dep_loc_emp_join AS ( SELECT e.employee_id , e.department_id , dl.department_name , dl.country_id FROM `HR.employees` AS e LEFT JOIN dep_loc_join AS dl ON e.department_id = dl.department_id ) SELECT country_id , department_name , COUNT(employee_id) AS employee_count FROM dep_loc_emp_join GROUP BY country_id, department_name;

예시 5
“employees” 테이블에서 “department_id” 별로 “salary”의 분산을 계산하고자 합니다.
일반적으로 분산을 계산할 때는
VARIANCE()
함수를 사용할 수 있습니다.
또한, 모집단의 분산을 구하려면 VAR_POP()
함수를 사용할 수 있습니다.VARIANCE()
함수를 이용하여 “department_id” 별 “salary”의 분산을 구하는 예제
SELECT DISTINCT department_id , ROUND(VARIANCE(salary), 2) AS salary_variance FROM `HR.employees` GROUP BY department_id ORDER BY department_id;

VAR_POP()
함수를 이용하여 “department_id” 별 “salary”의 분산을 구하는 예제
SELECT DISTINCT department_id , ROUND(VAR_POP(salary), 2) AS salary_variance FROM `HR.employees` GROUP BY department_id ORDER BY department_id;

그러나 분산을 계산하는 함수를 사용하고 싶지 않거나 사용할 수 없는 경우, 윈도우 함수를 활용하여 분산을 계산할 수 있습니다. 편차의 제곱의 평균이 분산임을 고려하여, 우선 편차의 합이 0임을 확인한 다음, 편차를 활용하여 분산을 계산할 수 있습니다.
WITH
문을 사용하는 경우, 사용하지 않는 경우를 구분하여 쿼리를 작성해 보겠습니다.WITH
문을 사용하는 경우- “department_id” 별 “salary”의 편차의 합을 구하는 예제입니다.
WITH employee_salary_dev AS ( SELECT employee_id , salary - AVG(salary) OVER() AS deviation FROM `HR.employees` ) SELECT ROUND(SUM(deviation), 2) AS deviations_sum FROM employee_salary_dev;

WITH employee_salary_dev AS ( SELECT department_id , salary - AVG(salary) OVER(PARTITION BY department_id) AS deviation , COUNT(salary) OVER(PARTITION BY department_id) AS n FROM `HR.employees` ) SELECT department_id , ROUND(SUM(CASE WHEN n = 1 THEN 0 ELSE (deviation * deviation) / (n - 1) END), 2) AS variance FROM employee_salary_dev GROUP BY department_id ORDER BY department_id;

WITH
문을 사용하지 않는 경우- “department_id” 별 “salary”의 편차의 합을 구하는 예제입니다.
SELECT ROUND(SUM(dev), 2) AS deviations_sum FROM ( SELECT salary - AVG(salary) OVER() AS dev FROM `HR.employees` );

SELECT department_id , ROUND(SUM(CASE WHEN n = 1 THEN 0 ELSE (deviation * deviation) / (n - 1) END), 2) AS variance FROM ( SELECT department_id , salary - AVG(salary) OVER(PARTITION BY department_id) AS deviation , COUNT(salary) OVER(PARTITION BY department_id) AS n FROM `HR.employees` ) GROUP BY department_id ORDER BY department_id;

10.2. 서브쿼리의 종류
서브쿼리의 종류로는 스칼라 서브쿼리, 행 서브쿼리, 열 서브쿼리, 테이블 서브쿼리 총 4가지가 있습니다.
- 스칼라 서브쿼리 (Scalar Subquery):
결과로 단일 값을 반환합니다. 일반적으로
SELECT
문의 열 값 또는 WHERE
절의 조건 값으로 사용되며 각 행에 대한 값은 계산하는데 사용할 수 있습니다.SELECT column1 , ( SELECT sub_column FROM sub_table WHERE condition ) AS alias FROM Table1;
- 행 서브쿼리 (Row Subquery):
결과로 하나의 행을 반환합니다. 여러 열의 값을 포함하는 단일 행을 반환하며 다중 컬럼의 비교를 위해
WHERE
절에서 사용할 수 있습니다.SELECT column1 , column2 FROM Table1 WHERE (column1, column2) = ( SELECT sub_column1 , sub_column2 FROM sub_table WHERE condition );
- 열 서브쿼리 (Column Subquery):
결과로 여러 값을 포함하는 단일 열을 반환합니다.
IN
, NOT IN
등의 연산자와 함께 WHERE
절에서 사용될 수 있습니다.SELECT column1 , column2 FROM Table1 WHERE column1 IN ( SELECT sub_column FROM sub_table WHERE condition );
- 테이블 서브쿼리 (Table Subquery):
결과로 여러 행과 열을 반환합니다. 주로
FROM
절에서 사용되며, 임시 테이블처럼 작동하여 JOIN
연산이나 인라인 뷰로 사용할 때 유용합니다.SELECT column1 , column2 FROM ( SELECT sub_column1 , sub_column2 FROM sub_table WHERE condition ) AS alias;
10.2.1. 스칼라
스칼라 서브쿼리는 결과가 단일 값을 반환하는 서브쿼리를 의미합니다. 이 서브쿼리는 주로
SELECT
절에서 사용되며, 결과의 각 행에 대해 서브쿼리를 실행하고 단일 값을 반환합니다. 스칼라 서브쿼리는 메인쿼리와 함께 실행되며, 서브쿼리가 여러 번 호출되는 경우 성능 문제를 초래할 위험이 있습니다. 대용량 데이터베이스에서 스칼라 서브쿼리를 사용하면 쿼리 실행 시간이 길어질 수 있어 대안으로 JOIN
혹은 WINDOW FUNCTION을 활용할 수 있습니다.기본 구문
SELECT column1 , column2 , ( SELECT single_value FROM sub_table WHERE conditions ) AS scalar_sub_column FROM main_table WHERE main_table_conditions;
설명
- main_table : 메인쿼리에서 사용하는 메인테이블입니다.
- sub_table : 스칼라 서브쿼리에서 사용하는 서브쿼리 테이블입니다.
- column1, column2 : 메인쿼리에서 선택한 열입니다.
- conditions : 메인쿼리 및 스칼라 서브쿼리의 조건을 지정하는 부분입니다.
- scalar_sub_column : 스칼라 서브쿼리가 반환한 단일 값을 저장하는 열입니다.
예시 1
각 직원의 부서별 최고 급여를 계산하고 “max_salary_for_department” 컬럼을 추가하여 값을 반환한 쿼리입니다.
SELECT e.employee_id , e.department_id , e.first_name || ' ' || e.last_name AS full_name , e.salary , ( SELECT MAX(salary) FROM `HR.employees` as ms WHERE department_id = e.department_id ORDER BY e.salary DESC ) AS max_salary_for_department FROM `HR.employees` AS e ORDER BY department_id;
WITH
절을 사용하여 공통 테이블인 “MaxSalaries” 를 정의합니다. 각 부서별 최고 급여를 계산한 결과를 기반으로 메인쿼리에서의 직원 정보와 부서별 최고 급여를 결합한 결과를 반환합니다. WITH
절에 선언한 공통 테이블은 재사용이 가능해 코드의 유지 및 수정을 용이하게 해줍니다.WITH MaxSalaries AS ( SELECT department_id , MAX(salary) AS max_salary FROM `HR.employees` GROUP BY department_id ) SELECT e.employee_id , e.department_id , e.first_name || ' ' || e.last_name AS full_name , e.salary , ms.max_salary AS max_salary_for_department FROM `HR.employees` AS e LEFT JOIN MaxSalaries AS ms ON e.department_id = ms.department_id ORDER BY e.department_id;

예시 2
“employees” 테이블과 ”jobs” 테이블을 활용하여 직원의 직무 명을 조회하는 쿼리입니다. 특정 직원의 “employee_id”를 사용하여 해당 직원의 직무 이름을 스칼라 값으로 조회합니다. 서브쿼리와
WITH
절을 사용한 쿼리 두 가지 경우를 살펴보겠습니다.SELECT employee_id , first_name || ' ' || last_name AS full_name , ( SELECT job_title FROM `HR.jobs` AS j WHERE j.job_id = e.job_id ) AS job_title FROM `HR.employees` AS e WHERE 1=1 , employee_id = 102;
WITH EmployeeJob AS ( SELECT e.employee_id , e.first_name || ' ' || e.last_name AS full_name , j.job_title FROM `HR.employees` AS e JOIN `HR.jobs` AS j ON e.job_id = j.job_id ) SELECT employee_id , full_name , job_title FROM EmployeeJob WHERE 1=1 , employee_id = 102;

10.2.2 인라인 뷰
인라인 뷰는
FROM
절 내에서 사용되는 서브쿼리를 의미합니다. 인라인 뷰는 쿼리 내에서 임시 테이블로 작동하며, 주 쿼리에서 해당 결과를 참조하여 작업을 수행합니다. 인라인 뷰는 단순화, 데이터 변형, 중간 결과 집합의 생성 등 다양한 목적으로 사용되고 SQL 쿼리의 효율성과 가독성을 향상시키는 데 매우 유용합니다. 이를 통해 쿼리의 복잡도를 줄이고, 중간 단계의 결과를 활용하여 최종 결과를 도출하는 데 도움을 줍니다.인라인 뷰 내에서
ORDER BY
를 사용할 수 있지만, 최종 결과 순서가 아니기 때문에 사용 시 주의해야 합니다. 서브쿼리 내 ORDER BY
는 최종 결과의 정렬을 결정하는 메인 쿼리의 일부가 아니기 때문입니다. 최종 결과의 정렬을 원한다면 서브쿼리가 아닌 메인쿼리에 ORDER BY
를 포함시켜야 합니다. 인라인 뷰는 임시 테이블처럼 사용할 수 있어 중간 결과 집합을 생성함으로써 복잡한 쿼리를 단순화할 수 있습니다.
- 임시 테이블처럼 작동: 인라인 뷰는 쿼리 실행 시 임시적으로 생성되며, 쿼리 실행이 완료되면 사라집니다.
- 복잡한 쿼리의 단순화: 인라인 뷰를 사용하면 복잡한 조인 조건이나 필터링 로직을 서브쿼리로 분리하여 주 쿼리를 간결하게 만들 수 있습니다.
- 다중 조인의 중간 단계: 여러 테이블을 조인할 때, 인라인 뷰를 사용하여 중간 결과 집합을 생성한 결과를 대상으로 추가 작업을 수행할 수 있습니다.
- 중간 결과 생성: 복잡한 쿼리를 분리하여 중간 단계의 결과를 생성하고, 이를 기반으로 최종 결과를 얻을 수 있습니다.
기본 구문
SELECT column1 , column2 FROM ( SELECT sub_column1 , sub_column2 FROM sub_table WHERE conditions ) AS inlineview_sub_column WHERE main_table_conditions;
설명
- inlineview_sub_column: 인라인 뷰의 별칭으로, 인라인 뷰의 결과를 참조할 때 사용됩니다.
- column1, column2: 메인쿼리에서 선택하려는 메인 테이블의 열입니다.
- sub_column1, sub_column2: 인라인 뷰 내에서 선택하려는 “Table"의 열입니다.
- sub_table: 인라인 뷰 내부의 서브쿼리에서 참조하는 테이블입니다.
- conditions: 인라인 뷰 내의 서브쿼리에서 데이터를 필터링하기 위해 사용되는 조건들입니다.
- main_table_conditions: 인라인 뷰 외부의 메인쿼리에서 데이터를 필터링하기 위해 사용되는 조건들입니다.
예시 1
“employees” 테이블의 평균 급여를 계산하는 서브쿼리가 인라인 뷰로 사용됩니다. 메인쿼리는 인라인 뷰의 결과를 참조하여 직원의 평균 급여보다 높은 급여를 받는 직원의 이름과 급여를 조회합니다. 서브쿼리는 주로 알려지지 않은 값을 먼저 조회하거나 계산한 뒤 그 결과를 메인쿼리에서 활용하기 위해 사용합니다.
아래 예시에서 볼 수 있듯이, “employees” 테이블의 전체 직원의 평균 급여를 미리 알 수 없습니다. 서브쿼리에서는 이 값을 알기 위해 먼저 평균 급여를 계산합니다. 계산된 평균 급여는 메인쿼리에서 인라인 뷰의 결과인 “avgs.avg_salary”에서 참조되며, 해당 값을 기준으로 급여가 평균보다 높은 직원들을 필터링하여 직원의 이름과 급여를 조회합니다.
SELECT e.first_name , e.salary FROM `HR.employees` AS e ( SELECT AVG(salary) AS avg_salary FROM `HR.employees` ) AS avgs WHERE e.salary > avgs.avg_salary;
WITH
절을 사용하면 복잡한 쿼리의 가독성이급여를 계산하는 서브쿼리의 결과를 참조하는 임시 이름입니다. WITH AverageSalary AS ( SELECT AVG(salary) AS avg_salary FROM `HR.employees` ) SELECT e.first_name , e.salary FROM `HR.employees` AS e JOIN AverageSalary avgs ON e.salary > avgs.avg_salary;
예시의 결과는 서브쿼리 결과와
WITH
절을 사용한 쿼리 결과와 같습니다.
예시 2
부서별로 가장 높은 급여를 받는 직원의 “first_name”, “department_id”, “salary”를 조회합니다.
아래 서브쿼리 예시는 각 “department_id”별로 최대 급여 “max_salary”를 계산하여 부서별로 가장 높은 급여 값을 반환합니다.
메인쿼리에서는 반환된 가장 높은 급여 값을
JOIN
연산을 통해 “employees” 테이블과 결합합니다. JOIN
연산은 “department_id”와 “salary” 기준으로 이루어지고, 결과를 바탕으로 각 부서에서 가장 높은 급여를 받는 직원의 정보만 필터링하여 조회합니다.SELECT e.first_name , e.department_id , e.salary FROM `HR.employees` AS e JOIN ( SELECT department_id , MAX(salary) AS max_salary FROM `HR.employees` GROUP BY department_id ) AS d ON e.department_id = d.department_id AND e.salary = d.max_salary;
각 부서의 최대 급여라는 알려지지 않은 기준값을 바탕으로 데이터를 조회하므로 서브쿼리를 활용하여 해당 값을 먼저 계산하고 그 결과를 메인쿼리에서 활용합니다. 이와 같이 서브쿼리는 알려지지 않은 기준으로 검색할 때 용이합니다.
WITH
절을 사용하면 “DepartmentMaxSalaries”라는 임시 이름으로 서브쿼리의 결과 집합을 참조할 수 있습니다. 이렇게 하면 쿼리의 가독성이 향상되며, 복잡한 쿼리의 구성을 좀 더 명확하게 파악할 수 있습니다.WITH DepartmentMaxSalaries AS ( SELECT department_id , MAX(salary) AS max_salary FROM `HR.employees` GROUP BY department_id ) SELECT e.first_name , e.department_id , e.salary FROM `HR.employees` AS e JOIN DepartmentMaxSalaries dms ON e.department_id = dms.department_id AND e.salary = dms.max_salary;
예시의 결과는 서브쿼리 결과와
WITH
절을 사용한 쿼리 결과와 같습니다.
예시 3
직책별 평균 급여와 해당 급여와 가장 가까운 급여를 받는 직원을 조회합니다.
아래 서브쿼리 예시에서 “employees” 테이블에서 “job_id”별 평균 급여를 계산하여 “iv”로 별칭을 합니다. 메인 쿼리에서는 해당 결과를
JOIN
연산하여 “employees”테이블과 결합합니다. JOIN
은 ON
절에서 “job_id” 기준으로 이루어지며 각 “job_id”에 대한 각 직원의 “salary”와 “avg_salary”가 함께 조회가 가능합니다. 마지막으로 ORDER BY
절에서 각 직원의 “salary”와 “avg_salary”의 차이가 큰 순서대로 결과를 내림차순으로 정렬하여 “job_id”의 평균 급여와 가장 큰 차이를 보이는 직원부터 조회할 수 있습니다.SELECT e.first_name , e.job_id , e.salary , iv.avg_salary FROM `HR.employees` AS e JOIN ( SELECT job_id , AVG(salary) AS avg_salary FROM `HR.employees` GROUP BY job_id ) AS iv ON e.job_id = iv.job_id ORDER BY ABS(e.salary - iv.avg_salary) DESC;
각 “job_id”의 “avg_salary”라는 알려지지 않은 기준을 바탕으로 데이터를 조회하므로, 서브쿼리를 활용하여 기준값을 먼저 계산한 후 메인쿼리에서 해당 결과를 활용할 수 있습니다.
WITH
절을 사용하면 서브쿼리의 결과 집합을 “JobAverageSalaries”라는 일시적인 이름으로 참조할 수 있습니다. 이 방식은 쿼리의 가독성을 높이며, 중복 코드를 줄여줍니다.WITH JobAverageSalaries AS ( SELECT job_id , AVG(salary) AS avg_salary FROM `HR.employees` GROUP BY job_id ) SELECT e.first_name , e.job_id , e.salary , jas.avg_salary FROM `HR.employees` AS e JOIN JobAverageSalaries AS jas ON e.job_id = jas.job_id ORDER BY ABS(e.salary - jas.avg_salary) DESC;
예시의 결과는 서브쿼리 결과와
WITH
절을 사용한 쿼리 결과와 같습니다.
10.2.3. 중첩
중첩 서브쿼리는 하나의 SQL 쿼리 안에 다른 서브쿼리가 포함된 쿼리 구조를 의미합니다. 일반적으로 중첩 서브쿼리는 외부 쿼리의 조건을 평가하거나 비교하기 위해 주로
WHERE
절에서 사용됩니다. 이때, 중첩 서브쿼리는 외부 쿼리의 조건에 따라 실행되며, 서브쿼리 내부에서는 외부 쿼리의 결과나 조건을 활용합니다.중첩 서브쿼리를 사용하면 한 쿼리 안에서 여러 수준의 서브쿼리를 사용하여 복잡한 조건을 처리하거나, 서브쿼리의 결과를 기반으로 다양한 계산이나 비교를 할 수 있습니다.
기본 구문
SELECT main_column1 FROM main_table WHERE main_column2 operator ( SELECT sub_column FROM sub_table WHERE sub_condition );
설명
- main_column1: 외부 쿼리에서 선택할 주요 열을 지정합니다.
- main_table: 외부 쿼리의 주 테이블을 지정합니다.
- main_column2: 외부 쿼리에서 조건으로 사용할 주요 열을 지정합니다.
- operator: 비교 연산자(예:
=
,>
,<
,IN
등)를 지정합니다.
- sub_column: 서브쿼리에서 선택할 열을 지정합니다.
- sub_table: 서브쿼리의 테이블을 지정합니다.
- sub_condition: 서브쿼리에서 사용되는 조건을 지정합니다.
예시 1
중첩 서브쿼리를 통해 “employees” 테이블에서 2005년 1월 1일 이후 입사한 직원들의 평균 급여보다 급여가 높은 직원의 아이디, 성, 이름, 입사일, 급여를 출력하는 쿼리입니다.
SELECT employee_id , first_name , last_name , hire_date , salary FROM `HR.employees` WHERE salary > ( SELECT AVG(salary) FROM `HR.employees` WHERE hire_date > '2005-01-01' );

예시 2
중첩 서브쿼리를 통해 “departments” 테이블에서 “manger_id” 컬럼이
NULL
인 “department_id”를 조회하고, “employees” 테이블에서 해당 “department_id”에 속하지 않은 직원의 “department_id”와 “employee_id”를 출력하는 쿼리입니다.SELECT department_id , employee_id FROM `HR.employees` WHERE department_id NOT IN ( SELECT department_id FROM `HR.departments` WHERE manager_id IS NULL );

예시 3
percentile 함수(백분위 함수)를 이용하여 “employees” 테이블에서 직원들의 급여(”salary”)가 상위 25%에 해당하는 값, 즉 75번째 백분위수를 찾고, 이보다 높은 급여를 받는 직원들의 아이디(”employee_id”)와 급여(”salary”)를 출력하는 쿼리입니다.
SELECT employee_id , salary FROM `HR.employees` WHERE salary > ( SELECT percentile_cont(salary, 0.75) OVER() FROM `HR.employees` LIMIT 1 ) ORDER BY salary DESC;

10.2.4. IN
IN
연산자는 서브쿼리의 결과 집합 안에 특정 값을 포함하거나 포함하지 않는지 확인합니다. NOT IN
연산자는 그 반대의 조건을 나타냅니다.기본 구문
value [ NOT ] IN ( subquery )
예시 1
다음은 직업명이 'Sales Representative', 'Marketing Manager'인 직원의 아이디와, 이름을 출력하는 쿼리입니다.
SELECT employee_id , first_name || ' ' || last_name AS full_name FROM `HR.employees` WHERE 1=1 , job_id IN ( SELECT job_id FROM `HR.jobs` WHERE 1=1 , job_title IN ('Sales Representative', 'Marketing Manager') );
WITH SelectedJobs AS ( SELECT job_id FROM `HR.jobs` WHERE job_title IN ('Sales Representative', 'Marketing Manager') ) SELECT e.employee_id , e.first_name || ' ' || e.last_name AS full_name FROM `HR.employees` AS e WHERE 1=1 , e.job_id IN ( SELECT job_id FROM SelectedJobs );

예시 2
“departments” 테이블에서 특정 부서 'Sales', 'Marketing'에 속하지 않은 직원을 찾는
NOT IN
서브쿼리입니다.SELECT employee_id, , first_name || ' ' || last_name AS full_name , department_id FROM `HR.employees` WHERE department_id NOT IN ( SELECT department_id FROM `HR.departments` WHERE 1=1 , department_name IN ('Sales', 'Marketing') );
WITH ExcludedDepartments AS ( SELECT department_id FROM `HR.departments` WHERE department_name IN ('Sales', 'Marketing') ) SELECT e.employee_id , e.first_name || ' ' || e.last_name AS full_name , e.department_id FROM `HR.employees` AS e WHERE 1=1 , e.department_id NOT IN ( SELECT department_id FROM ExcludedDepartments );

10.2.5. EXISTS
EXISTS
는 SQL에서 사용되는 논리 연산자로, 서브쿼리의 결과가 존재하는지 여부를 확인하는 데 사용됩니다. EXISTS
서브쿼리는 주로 WHERE
절에서 사용되며, 외부 쿼리의 각 행에 대해 서브쿼리를 실행하여 참 또는 거짓을 판단합니다. 서브쿼리 내에서는 일반적으로 SELECT
문이 아니라 단순한 조건을 사용하여 해당 조건을 만족하는 데이터가 있는지를 확인합니다. 서브쿼리의 결과가 비어있지 않으면 TRUE
를 반환하고, 결과가 비어있으면 FALSE
를 반환합니다.기본 구문
SELECT
절에서의 구문SELECT EXISTS ( SELECT 'found' FROM sub_table WHERE condition ) AS exists_result;
WHERE
절에서의 구문SELECT column1 , column2 FROM main_table WHERE EXISTS ( SELECT 'found' FROM sub_table WHERE condition );
설명
- column1, column2: 반환할 열을 지정합니다.
- main_table: 데이터를 조회할 기본 테이블을 지정합니다.
- sub_table: 서브쿼리에서 사용할 두 번째 테이블을 지정합니다.
- found: 서브쿼리에서 사용된 값으로, 실제로는 항상 참(
TRUE
)으로 평가되는 임의의 값입니다. 이 값은EXISTS
서브쿼리에서 조건이 참인지 여부를 확인하는 데 사용됩니다.
- condition: 서브쿼리에서 사용할 조건을 지정합니다.
예시
EXISTS
서브쿼리를 통해 “departments” 테이블의 부서 아이디가 50인 조건을 만족하는 행이 하나라도 존재한다면, “employees” 테이블의 직원 아이디, 직업 아이디, 부서 아이디를 출력하는 쿼리입니다.SELECT employee_id , job_id , department_id FROM `HR.employees` WHERE EXISTS ( SELECT 1 FROM `HR.departments` WHERE department_id = 50 )

10.2.6. 상관
상관 서브쿼리는 서브쿼리가 외부 쿼리의 열을 참조하여 실행되는 경우를 의미합니다. 일반적인 서브쿼리가 단순히 외부 쿼리에서 서브쿼리의 결과를 이용하는 것과 달리, 상관 서브쿼리는 외부 쿼리와 서브쿼리가 서로 연관성을 갖고 있기 때문에 서브쿼리의 결과가 외부 쿼리의 실행에 영향을 미치게 됩니다.
SELECT
절에서의 구문SELECT m.column1 , m.column2 ( SELECT s.value FROM sub_table s WHERE s.id = m.id ) AS subquery_result FROM main_table AS m;
WHERE
절에서의 구문SELECT m.column1 , m.column2 FROM main_table m WHERE operator ( SELECT s.value FROM sub_table s WHERE m.id = s.id );
설명
- main_table (m): 데이터를 조회할 기본 테이블을 지정합니다.
- sub_table (s): 서브쿼리에서 사용할 두 번째 테이블을 지정합니다.
- m.id: 메인 테이블 “main_table”의 id 열을 나타냅니다.
- s.id: 서브 테이블 “sub_table”의 id 열을 나타냅니다. 이 열은 외부 쿼리의 메인 테이블과 서브쿼리의 서브 테이블을 연결하는 데 사용됩니다.
- operator: 비교 연산자(예:
=
,>
,<
,IN
등)를 지정합니다.
예시
상관 서브쿼리 내의 “job_history” 테이블의 직원 아이디와 외부 쿼리의 “employees” 테이블의 직원 아이디가 같은 직원의 성과 이름 및 직원 아이디를 출력하는 쿼리입니다.
SELECT e.first_name , e.last_name , e.employee_id FROM `HR.employees` AS e WHERE EXISTS ( SELECT employee_id FROM `HR.job_history` AS j WHERE j.employee_id = e.employee_id );

다음과 같이, “employees” 테이블의 직원 아이디(“employee_id”) 중 “job_history” 테이블에 속한 직원 아이디(“employee_id”)와 일치하는 사원의 이름(“first_name”), 성(”last_name”) 및 직원 아이디(”employee_id”)를 확인할 수 있습니다.
10.2.7. ARRAY
배열 형태로 결과를 나타내는 특별한 서브쿼리 표현 방법입니다. 결과가 0행 일 때 빈 배열을 반환하고
NULL
배열은 반환하지 않습니다. ARRAY
서브쿼리는 반환 배열에 대한 유형이 컬럼 당 단일한 유형이어야 합니다. 그렇지 않으면 에러가 발생합니다. SELECT AS STRUCT
와 함께 작성하려면 SELECT
목록에 여러 개의 열이 포함될 수 있으며, 배열 서브쿼리의 값은 STRUCT
의 배열 구조로 반환됩니다. SELECT AS
없이 여러 열을 선택할 경우 에러가 발생합니다.기본 구문
ARRAY ( subquery )
예시 1
“job_id”가 'SA_MAN'인 직원들의 “first_name”을 배열로 선택하는 쿼리입니다.
SELECT ARRAY( SELECT first_name FROM `HR.employees` WHERE job_id='SA_MAN' ) AS SA_MAN;
WITH SaManEmployees AS ( SELECT ARRAY_AGG(first_name) AS SA_MAN FROM `HR.employees` WHERE job_id = 'SA_MAN' ) SELECT SA_MAN FROM SaManEmployees;

예시 2
부서별 직원 목록을 배열로 생성하는 쿼리입니다.
GROUP BY
와 함께 사용할 경우 ARRAY_AGG
로 사용합니다.SELECT department_id , ARRAY_AGG(first_name) AS employees_name FROM `HR.employees` GROUP BY department_id;
WITH DepartmentEmployees AS ( SELECT department_id , ARRAY_AGG(first_name) AS employees_name FROM `HR.employees` GROUP BY department_id ) SELECT department_id , employees_name FROM DepartmentEmployee;
