PromleeBlog
sitemap
aboutMe

posting thumbnail
SQL 조회 문법 총정리 3편 (MySQL/Oracle CTE, 서브쿼리, 고급 집계)
Advanced SQL Query Syntax Part 3 (MySQL/Oracle CTE, Subquery, Advanced Aggregation)

📅

🚀

들어가기 전에 🔗

안녕하세요.
SQL 조회 문법 총정리 1편과 2편
을 통해 기본적인 데이터 조회부터 문자열 및 날짜 처리, 다양한 JOIN, 그리고 윈도우 함수의 기초까지 함께 살펴보았습니다.
이번 3편에서는 특히 복잡한 쿼리를 명쾌하게 만드는 CTE(Common Table Expressions)와 쿼리 속의 또 다른 쿼리인 서브쿼리(Subquery)를 집중적으로 파헤치고, 고급 집계 함수 중 일부를 함께 살펴보겠습니다.
1, 2편에서 사용했던 Users 테이블과 Orders 테이블을 계속 사용합니다.

🚀

WITH 절 (Common Table Expressions, CTE) 🔗

복잡한 SQL 쿼리를 작성할 때 가독성을 높이고 구조적으로 만들 수 있도록, 쿼리 내에서 임시로 이름을 가진 결과 집합을 정의하고 사용하는 기능입니다.
CTE는 쿼리를 여러 논리적 단위로 나누어 관리할 수 있게 해주며, 특히 여러 번 참조되거나 복잡한 서브쿼리를 대체할 때 유용합니다.

예시 1
각 사용자별 총 주문 금액을 계산한 후, 평균 주문 금액 이상인 사용자 찾기
-- UserTotalAmounts CTE: 사용자별 총 주문 금액 계산
WITH UserTotalAmounts AS (
    SELECT
        user_id,                      -- 사용자 ID
        SUM(amount) AS total_amount   -- 해당 사용자의 총 주문 금액
    FROM Orders
    WHERE user_id IS NOT NULL         -- user_id가 NULL이 아닌 주문만 대상
    GROUP BY user_id                  -- 사용자 ID로 그룹화
),
 
-- AvgTotalAmount CTE: 전체 사용자의 평균 총 주문 금액 계산
AvgTotalAmount AS (
    -- UserTotalAmounts CTE의 결과를 사용하여 평균 계산
    SELECT AVG(total_amount) AS avg_amount
    FROM UserTotalAmounts
)
 
-- 메인 쿼리: 평균 총 주문 금액 이상인 사용자 정보 조회
SELECT
    U.name,                       -- 사용자 이름
    UTA.total_amount              -- 사용자의 총 주문 금액
FROM Users U
JOIN UserTotalAmounts UTA ON U.id = UTA.user_id -- Users 테이블과 UserTotalAmounts CTE를 사용자 ID로 조인
WHERE UTA.total_amount >= (SELECT avg_amount FROM AvgTotalAmount);
-- AvgTotalAmount CTE의 결과를 서브쿼리로 사용하여 조건 비교
nametotal_amount
홍길동16000
(평균 계산 결과에 따라 달라질 수 있음)
예시 2
여러 CTE를 연결하여 사용하는 경우
-- OrdersByProduct CTE: 상품별 총 주문액과 주문 건수 계산
WITH OrdersByProduct AS (
    SELECT
        product,                      -- 상품명
        SUM(amount) AS total_product_amount, -- 상품별 총 주문액
        COUNT(id) AS order_count      -- 상품별 주문 건수
    FROM Orders
    GROUP BY product
),
 
-- UsersWithHighOrderCountProduct CTE: 특정 주문 건수(예: 2건) 이상인 상품을 주문한 사용자 ID 조회
UsersWithHighOrderCountProduct AS (
    SELECT DISTINCT -- 중복 제거
        O.user_id
    FROM Orders O
    JOIN OrdersByProduct OP ON O.product = OP.product -- Orders와 OrdersByProduct CTE 조인
    WHERE OP.order_count >= 2 AND O.user_id IS NOT NULL -- 주문 건수가 2 이상인 상품을 주문한 사용자
)
 
-- 메인 쿼리: 해당 사용자들의 이름과 이메일 조회
SELECT
    U.name,
    U.email
FROM Users U
JOIN UsersWithHighOrderCountProduct UHCOP ON U.id = UHCOP.user_id; -- Users와 두 번째 CTE 조인
(Apple, Banana가 2건 이상 주문되었고, 해당 상품을 주문한 사용자들)
nameemail
홍길동hong@example.com
이동훈donghoon099@naver.com
햄랑이hamlang@ham.ham

재귀 CTE (Recursive CTE) 🔗

계층형 데이터 조회(예: 조직도, 부품 전개, 친구 관계 탐색) 등에 사용되며, 초기 멤버(Anchor Member)와 재귀 멤버(Recursive Member)를 UNION ALL로 연결합니다.
재귀 멤버는 자기 자신 CTE를 참조합니다.

예시
Employees 테이블(2편 예제)에서 특정 직원(예: Alice) 하위의 모든 직원 계층 조회하기 (Employees 테이블: id, name, manager_id)
idnamemanager_idlvl (또는 level)
1Alice0
2Bob11
3Charlie11
4David22

🚀

서브쿼리 (Subquery) 🔗

서브쿼리는 다른 SQL 쿼리 내부에 포함된 또 다른 SELECT 문입니다.
메인 쿼리가 실행되기 전에 한 번 실행되어 그 결과를 메인 쿼리에서 활용하거나(비상관 서브쿼리), 메인 쿼리의 각 행에 대해 반복적으로 실행될 수 있습니다(상관 서브쿼리).
복잡한 조건을 설정하거나, 동적인 데이터를 기반으로 조회할 때 유용하게 사용됩니다.

서브쿼리 사용 위치 및 종류 🔗


비상관 서브쿼리
(Non-correlated Subquery): 외부 쿼리와 독립적으로 실행되어 결과를 반환.
상관 서브쿼리
(Correlated Subquery): 외부 쿼리의 컬럼을 참조하여, 외부 쿼리의 각 행에 대해 반복적으로 실행될 수 있음. 성능에 주의.

SELECT 절에서의 서브쿼리 (스칼라 서브쿼리) 🔗

메인 쿼리의 각 행에 대해 독립적으로 실행되어 값을 반환하며, 이 값은 메인 쿼리의 컬럼처럼 사용됩니다.

예시
각 사용자(Users) 정보와 함께 해당 사용자의 총 주문 횟수를 조회 (주문 없는 사용자는 0회)
SELECT
    U.name, -- 사용자 이름
    U.email, -- 사용자 이메일
    -- 스칼라 서브쿼리: 현재 행의 U.id를 참조하여 해당 사용자의 총 주문 횟수 계산
    (SELECT COUNT(O.id)
      FROM Orders O
      WHERE O.user_id = U.id) AS order_count -- 계산된 주문 횟수를 order_count 컬럼으로 표시
FROM Users U;
nameemailorder_count
홍길동hong@example.com4
이동훈donghoon099@naver.com2
햄랑이hamlang@ham.ham2
홍길은1
제임스james@example.com1
이몽룡mong@another.com0

FROM 절에서의 서브쿼리 (인라인 뷰) 🔗

서브쿼리의 결과를 하나의 임시 테이블(인라인 뷰)처럼 취급하여 메인 쿼리에서 사용할 수 있게 합니다. 반드시 별칭(alias)을 지정해야 합니다.
인라인 뷰는 서브쿼리의 결과를 테이블처럼 사용하기 때문에, 메인 쿼리에서 조인, 필터링 등 다양한 작업을 수행할 수 있습니다.

예시
각 사용자의 평균 주문 금액보다 높은 금액의 주문들만 조회
-- 메인 쿼리: 주문 정보와 사용자 이름 조회
SELECT
    U.name,         -- 사용자 이름
    UserAvgOrders.product, -- 인라인 뷰에서 가져온 상품명
    UserAvgOrders.amount   -- 인라인 뷰에서 가져온 주문 금액
FROM Users U
-- 인라인 뷰 (UserAvgOrders): 사용자별 평균 주문 금액보다 큰 주문들만 필터링
JOIN (
    SELECT
        O.user_id,    -- 사용자 ID
        O.product,    -- 상품명
        O.amount,     -- 주문 금액
        -- 각 사용자별(PARTITION BY O.user_id) 평균 주문 금액을 계산하는 윈도우 함수
        AVG(O.amount) OVER (PARTITION BY O.user_id) AS avg_user_amount
    FROM Orders O
    WHERE O.user_id IS NOT NULL
) UserAvgOrders ON U.id = UserAvgOrders.user_id -- Users 테이블과 인라인 뷰(UserAvgOrders)를 사용자 ID로 조인
WHERE UserAvgOrders.amount > UserAvgOrders.avg_user_amount; -- 조건: 주문 금액이 해당 사용자 평균 주문 금액보다 큰 경우
nameproductamount
홍길동Strawberry8000
햄랑이Melon6000
각 사용자의 평균 주문액을 초과하는 주문들만 나옴. 예시 데이터에 따라 결과가 달라질 수 있음

WHERE 절 / HAVING 절에서의 서브쿼리 🔗

조건을 비교하기 위해 사용되며, 반환 결과에 따라 단일 행/다중 행 연산자와 함께 사용됩니다.

예시 1 (단일 행 서브쿼리)
가장 나이가 많은 사용자의 이름과 나이 조회
SELECT
    name, -- 사용자 이름
    age   -- 사용자 나이
FROM Users
WHERE age = (SELECT MAX(age) FROM Users);
-- 조건: 나이가 (Users 테이블의 최대 나이)와 같은 경우
nameage
제임스30

예시 2 (다중 행 서브쿼리 - IN 사용)
'Apple' 또는 'Banana'를 주문한 모든 사용자의 이름 조회 (중복 제거)
SELECT DISTINCT -- 중복된 사용자 이름 제거
    U.name -- 사용자 이름
FROM Users U
-- 조건: 사용자 ID가 (아래 서브쿼리 결과 목록: 'Apple' 또는 'Banana'를 주문한 사용자 ID들)에 포함되는 경우
WHERE U.id IN (
    SELECT O.user_id
    FROM Orders O
    WHERE O.product IN ('Apple', 'Banana') AND O.user_id IS NOT NULL
);
name
홍길동
이동훈
제임스
예시 3 (다중 행 서브쿼리 - ANY 사용)
'홍길동'이 주문한 어떤 상품의 주문 금액보다 큰 주문 금액을 가진 다른 주문들 조회
SELECT
    O.id AS order_id,
    O.product,
    O.amount
FROM Orders O
WHERE O.amount > ANY ( -- 조건: 주문 금액이 (홍길동의 주문 금액들 중 '어떤 것'보다라도) 큰 경우
    SELECT amount
    FROM Orders
    WHERE user_id = (SELECT id FROM Users WHERE name = '홍길동')
)
AND O.user_id != (SELECT id FROM Users WHERE name = '홍길동'); -- 홍길동 본인의 주문은 제외
(홍길동의 주문 금액들 중 최솟값보다 큰 다른 주문들이 조회됨)
예시 4 (상관 서브쿼리 - EXISTS 사용)
한 번이라도 주문한 적이 있는 사용자 목록 조회
SELECT
    U.name,  -- 사용자 이름
    U.email  -- 사용자 이메일
FROM Users U
-- 조건: 다음 서브쿼리의 결과가 하나라도 존재하면 TRUE (상관 서브쿼리)
WHERE EXISTS (
    SELECT 1 -- 실제 값을 가져올 필요 없이 존재 여부만 체크
    FROM Orders O
    WHERE O.user_id = U.id -- 외부 쿼리 Users 테이블의 현재 행 U.id를 참조
);

🚀

집계 함수 활용 🔗

단순한 합계, 평균, 개수를 넘어 더 다양한 분석을 가능하게 하는 집계 방법 중 일부를 먼저 살펴보겠습니다.

ROLLUP: 계층적 소계 및 총계 생성 🔗

GROUP BY 절과 함께 사용되어 지정된 그룹핑 컬럼들의 계층 구조에 따라 소계와 총계를 생성합니다.

예시
사용자(Users.name)별, 상품(Orders.product)별 주문 총액 및 각 사용자별 총액, 전체 총액 보기
user_nameproduct_nametotal_sales
이동훈Banana2000
이동훈Grape3000
이동훈NULL5000
...
(결과는 여러 행으로 나타나며, user_name 또는 product_name이 '전체 총계' 또는 '사용자별 소계'인 행들이 소계/총계를 나타냅니다.)

문자열 집계: 여러 문자열 값을 하나로 합치기 🔗

그룹 내의 여러 문자열 값을 하나의 문자열로 결합합니다.

예시
각 사용자(Users.name)가 주문한 상품 목록(Orders.product)을 콤마로 구분하여 나열하기 (중복 없이, 상품명 오름차순)
nameproducts_ordered
이동훈Banana, Grape
제임스Banana
햄랑이Melon, Pineapple
홍길동Apple, Orange, Strawberry
홍길은Watermelon

🚀

결론 🔗

이번 3편에서는 CTE와 서브쿼리를 중심으로 복잡한 쿼리를 구조화하고 데이터를 효과적으로 가져오는 방법을 살펴보았습니다.
또한, 고급 집계 함수의 일부인 ROLLUP과 문자열 집계 함수도 함께 다루었습니다.
다음 편에서는 오늘 다루지 못한 나머지 고급 집계 함수(CUBE, GROUPING SETS), 강력한 문자열 처리를 위한 정규표현식, 윈도우 함수의 더 깊이 있는 활용법(분석 함수, 프레임 절 심화), 그리고 데이터를 행에서 열로, 또는 열에서 행으로 변환하는 피벗팅과 언피벗팅 기법에 대해 자세히 알아보겠습니다.

참고 🔗