다른 테이블의 결과를 조건으로 씁니다. IN, EXISTS, 비교 연산자와 함께 쓰입니다. 가장 흔한 형태입니다.
이 파트에서 다루는 내용
서브쿼리는 놓이는 위치로 구분합니다
서브쿼리를 종류별로 외우기보다, 어디에 놓였는지로 보면 역할이 분명해집니다. 세 자리가 있고 각각 하는 일이 다릅니다.
서브쿼리 결과를 테이블처럼 씁니다. 집계한 결과를 다시 조인하거나 필터할 때 필수적입니다.
행마다 값 하나를 가져옵니다. 편하지만 성능 함정이 있어 다음 섹션에서 따로 다룹니다.
-- 주문 금액 합계가 100만 원을 넘는 고객만
SELECT c.customer_nm, s.total_amount
FROM customer c
JOIN (SELECT customer_id, SUM(amount) AS total_amount
FROM orders
WHERE status_cd = 'DONE'
GROUP BY customer_id) s
ON s.customer_id = c.customer_id
WHERE s.total_amount > 1000000;집계 결과를 조건으로 쓰려면 인라인뷰나 HAVING이 필요합니다. 집계 전에는 존재하지 않는 값이기 때문입니다.
스칼라 서브쿼리는 편한 만큼 위험합니다
SELECT 절에 서브쿼리를 넣으면 조인 없이 다른 테이블 값을 가져올 수 있어 편합니다. 하지만 원칙적으로 결과 행마다 한 번씩 실행됩니다.
결과가 10건이면 문제없지만 10만 건이면 서브쿼리가 10만 번 실행됩니다. 조회 화면이 갑자기 느려졌을 때 흔한 원인입니다.
Oracle은 같은 입력값에 대한 결과를 캐싱해서 실행 횟수를 줄여 줍니다. 그래서 입력값의 종류가 적으면 빠르고, 값이 다양하면 캐시가 안 먹혀 느려집니다. 이 차이 때문에 개발 환경에서는 빠르다가 운영에서 느려지는 일이 생깁니다.
-- 행마다 실행될 수 있는 형태
SELECT o.order_id,
o.amount,
(SELECT c.customer_nm FROM customer c
WHERE c.customer_id = o.customer_id) AS customer_nm
FROM orders o
WHERE o.order_dt >= TRUNC(SYSDATE) - 7;
-- 조인으로 바꾸면 한 번에 처리됩니다
SELECT o.order_id, o.amount, c.customer_nm
FROM orders o
LEFT JOIN customer c ON c.customer_id = o.customer_id
WHERE o.order_dt >= TRUNC(SYSDATE) - 7;아우터 조인으로 바꿔야 결과가 같습니다. 스칼라 서브쿼리는 짝이 없으면 NULL을 반환하므로 LEFT JOIN에 대응합니다. INNER JOIN으로 바꾸면 행이 사라질 수 있습니다.
결과 행이 적고 서브쿼리 입력값 종류가 적으면 스칼라 서브쿼리가 읽기 좋습니다. 결과 행이 많거나 값이 다양하면 조인으로 바꿉니다. 두 행 이상이 반환되면 ORA-01427 오류가 나는 점도 기억합니다.
집합 연산은 UNION ALL을 기본으로 씁니다
두 결과를 그냥 이어 붙입니다. 중복 제거를 안 하므로 빠릅니다. 기본으로 이걸 쓰고, 중복 제거가 정말 필요할 때만 UNION으로 바꿉니다.
정렬해서 중복을 제거합니다. 그만큼 비용이 큽니다. 애초에 중복이 없는 데이터라면 낭비입니다.
앞의 결과에서 뒤의 결과를 뺍니다. 두 시스템 데이터를 대사할 때 유용합니다. 표준 SQL의 EXCEPT에 해당하는 Oracle 문법입니다.
양쪽에 다 있는 것만 남깁니다. MINUS와 함께 데이터 검증에 자주 쓰입니다.
-- 이관 전후 데이터가 같은지 양방향으로 확인
SELECT customer_id, customer_nm FROM customer_old
MINUS
SELECT customer_id, customer_nm FROM customer_new;
SELECT customer_id, customer_nm FROM customer_new
MINUS
SELECT customer_id, customer_nm FROM customer_old;양쪽 모두 0건이어야 완전히 같습니다. 한 방향만 확인하면 신규 시스템에만 있는 데이터를 놓칩니다. 이관 검증에서 실제로 쓰는 방법입니다.
WITH 절로 쿼리를 단계로 나눕니다
인라인뷰가 중첩되면 읽기 어려워집니다. WITH 절을 쓰면 각 단계에 이름을 붙여 위에서 아래로 읽히게 만들 수 있습니다.
같은 서브쿼리를 여러 번 참조할 때도 유용합니다. Oracle은 필요하다고 판단하면 결과를 임시 저장해 재사용합니다.
WITH recent_orders AS (
SELECT customer_id, amount
FROM orders
WHERE order_dt >= ADD_MONTHS(TRUNC(SYSDATE), -3)
AND status_cd = 'DONE'
),
customer_summary AS (
SELECT customer_id,
COUNT(*) AS order_cnt,
SUM(amount) AS total_amount
FROM recent_orders
GROUP BY customer_id
)
SELECT c.customer_nm,
c.grade_cd,
s.order_cnt,
s.total_amount
FROM customer_summary s
JOIN customer c ON c.customer_id = s.customer_id
WHERE s.total_amount > 1000000
ORDER BY s.total_amount DESC;같은 내용을 인라인뷰 중첩으로 쓰면 안쪽부터 읽어야 해서 이해가 어렵습니다. 단계가 둘 이상이면 WITH 절을 권합니다.
MERGE로 있으면 수정, 없으면 입력을 한 번에
배치에서 가장 흔한 요구사항입니다. 기존 데이터가 있으면 갱신하고 없으면 새로 넣는 처리입니다.
애플리케이션에서 조회 후 분기하면 건수만큼 왕복이 생기고, 동시 실행 시 중복 입력 문제도 생깁니다. MERGE는 한 문장으로 처리합니다.
MERGE INTO customer_summary t
USING (
SELECT customer_id,
COUNT(*) AS order_cnt,
SUM(amount) AS total_amount
FROM orders
WHERE order_dt >= TRUNC(SYSDATE) - 1
GROUP BY customer_id
) s
ON (t.customer_id = s.customer_id)
WHEN MATCHED THEN
UPDATE SET t.order_cnt = s.order_cnt,
t.total_amount = s.total_amount,
t.upd_dt = SYSDATE
WHEN NOT MATCHED THEN
INSERT (customer_id, order_cnt, total_amount, upd_dt)
VALUES (s.customer_id, s.order_cnt, s.total_amount, SYSDATE);ON 절의 컬럼은 UPDATE에서 바꿀 수 없습니다. 또 ON 조건에 맞는 대상 행이 여러 개면 ORA-30926 이 납니다. USING 쪽 결과에 중복 키가 없는지 먼저 확인합니다.
MERGE는 Oracle과 MSSQL에는 있지만 MySQL은 INSERT ... ON DUPLICATE KEY UPDATE, PostgreSQL은 INSERT ... ON CONFLICT 로 문법이 완전히 다릅니다. 부록 대조표에 정리해 두었습니다.