카테고리 없음

[SQL BOOSTER] 4.1 서브쿼리

garamdev 2025. 3. 9. 21:44

📚 SQL BOOSTER (유일환, DBian)

📌오라클 → PostgreSql 15로 변환

 

CHAP4. 유용한 SQL 문법

4.1.1 서브쿼리의 종류

서브쿼리는 성능이 좋지 못할 수 있다.

가장 조심할 부분은 무분별한 서브쿼리 남발이다.

  • SELECT 절의 단독 서브쿼리
  • SELECT 절의 상관 서브쿼리
  • WHERE 절의 단독 서브쿼리
  • WHERE 절의 상관 서브쿼리
  • 인라인ㅡ뷰

단독 서브쿼리 : 메인 SQL과 상관없이 실행 할 수 있는 서브쿼리

상관 서브쿼리 : 메인 SQL에서 값을 받아서 처리해야 하는 서브쿼리

(메인 SQL : 서브쿼리를 제외한 나머지 SQL)

4.1.2 SELECT 절의 단독 서브쿼리

-- ************************************************
-- PART I - 4.1.2 SQL1
-- ************************************************

-- 17년8월 총 주문금액 구하기 – SELECT절 단독 서브쿼리
select	TO_CHAR(T1.ORD_DT, 'YYYYMMDD') as ORD_YMD,
		SUM(T1.ORD_AMT) as ORD_AMT,
		(
			select	SUM(A.ORD_AMT)
			from	T_ORD A
			where 	A.ORD_DT >= DATE '2017-08-01'
			and		A.ORD_DT < DATE '2017-09-01'
		) as TOTAL_ORD_AMT -- SELECT 절의 단독 서브쿼리
from	T_ORD T1
where	T1.ORD_DT >= DATE '2017-08-01'
and		T1.ORD_DT < DATE '2017-09-01'
group by TO_CHAR(T1.ORD_DT, 'YYYYMMDD');


-- ************************************************
-- PART I - 4.1.2 SQL2
-- ************************************************

-- 17년8월 총 주문금액, 주문일자의 주문금액비율 구하기 – SELECT절 단독 서브쿼리
-- 주문금액 비율 = 주문일자별 주문금액(ORD_AMT) / 17년8월 주문 총 금액(TOTAL_ORD_AMT) * 100.00
-- T_ORD 테이블을 불필요하게 반복 접근하는 쿼리
-- 성능에 문제도 있지만, SQL을 변경할 때 손이 많이 가서 번거로움
-- 총 주문금액의 기준이 바뀌면, 두 서브쿼리를 모두 변경해야 함
select 	TO_CHAR(T1.ORD_DT, 'YYYYMMDD') as ORD_YMD,
		SUM(T1.ORD_AMT) as ORD_AMT,
		(
			select	SUM(A.ORD_AMT)
			from	T_ORD A
			where	A.ORD_DT >= DATE '2017-08-01'
			and		A.ORD_DT < DATE '2017-09-01'
		) as TOTAL_ORD_AMT,
		ROUND(
			SUM(T1.ORD_AMT) / (
				select	SUM(A.ORD_AMT)
				from	T_ORD A
				where 	A.ORD_DT >= DATE '2017-08-01'
				and		A.ORD_DT < DATE '2017-09-01'
			) * 100, 2
		) as ORD_AMT_RT
from	T_ORD T1
where	T1.ORD_DT >= DATE '2017-08-01'
and		T1.ORD_DT < DATE '2017-09-01'
group by TO_CHAR(T1.ORD_DT, 'YYYYMMDD');


-- ************************************************
-- PART I - 4.1.2 SQL3
-- ************************************************

-- 인라인-뷰를 사용해 반복 서브쿼리를 제거하는 방법
-- 같은 서브쿼리를 반복해서 사용해야 한다면 인라인ㅡ뷰를 고민하기
select	T1.ORD_YMD,
		T1.ORD_AMT,
		T1.TOTAL_ORD_AMT,
		ROUND(T1.ORD_AMT / T1.TOTAL_ORD_AMT * 100, 2) as ORD_AMT_RT
from	(
	select	TO_CHAR(T1.ORD_DT, 'YYYYMMDD') as ORD_YMD,
			SUM(T1.ORD_AMT) as ORD_AMT,
			(
				select	SUM(A.ORD_AMT)
				from	T_ORD A
				where	A.ORD_DT >= DATE '2017-08-01'
				and		A.ORD_DT < DATE '2017-09-01'
			) as TOTAL_ORD_AMT
	from	T_ORD T1
	where	T1.ORD_DT >= DATE '2017-08-01'
	and		T1.ORD_DT < DATE '2017-09-01'
	group by TO_CHAR(T1.ORD_DT, 'YYYYMMDD')
) T1;


-- ************************************************
-- PART I - 4.1.2 SQL4
-- ************************************************

-- 카테시안-조인을 사용해 반복 서브쿼리를 제거하는 방법
select	TO_CHAR(T1.ORD_DT, 'YYYYMMDD') as ORD_YMD
		,SUM(T1.ORD_AMT) as ORD_AMT
		,MAX(T2.TOTAL_ORD_AMT) as TOTAL_ORD_AMT
		,ROUND(SUM(T1.ORD_AMT) / MAX(T2.TOTAL_ORD_AMT) * 100, 2) as ORD_AMT_RT
from	T_ORD T1
		,(	select	SUM(A.ORD_AMT) as TOTAL_ORD_AMT
			from	T_ORD A
			where	A.ORD_DT >= TO_TIMESTAMP('2017-08-01', 'YYYY-MM-DD')
			and		A.ORD_DT < TO_TIMESTAMP('2017-09-01', 'YYYY-MM-DD')
		) T2
where	T1.ORD_DT >= TO_TIMESTAMP('2017-08-01', 'YYYY-MM-DD')
and		T1.ORD_DT < TO_TIMESTAMP('2017-09-01', 'YYYY-MM-DD')
group by TO_CHAR(T1.ORD_DT, 'YYYYMMDD');

-- SELECT 절의 단독 서브쿼리
-- 인라인ㅡ뷰
-- 카테시안ㅡ조인

-- 서브쿼리를 제거하면 SQL 성능이 좋아질 수 있음

-- 비슷하면서 약간씩 다른 서브쿼리를 매우 많이 사용하는 SQL
-- 인덱스나 힌트로는 성능 개선에 한계가 있음
-- SQL 전체를 뜯어고쳐서 반복되는 서브쿼리를 제거해야 함

4.1.3 SELECT 절의 상관 서브쿼리

메인 SQL에서 값을 받아 처리한다.

대부분의 조인을 해결할 수 있다.

적절하게 사용하기

-- ************************************************
-- PART I - 4.1.3 SQL1
-- ************************************************

-- 코드값을 가져오는 SELECT 절 상관 서브쿼리
-- 코드명 처리는 조인보다는 SELECT 절의 상관쿼리를 사용하는 것이 일반적
-- 코드처럼 값의 종류가 많지 않은 경우는 캐싱 효과로 성능이 더 좋아질 수도 있음
select	T1.ITM_TP
		,(select	A.BAS_CD_NM
		  from		C_BAS_CD A -- 기준코드 테이블
		  where 	A.BAS_CD_DV = 'ITM_TP'
		  	and		A.BAS_CD = T1.ITM_TP
		  	and		A.LNG_CD = 'KO'
		) as ITM_TP_NM -- 메인 SQL의 값을 가져와 사용
		,T1.ITM_ID
		,T1.ITM_NM
from	M_ITM T1;


-- ************************************************
-- PART I - 4.1.3 SQL2
-- ************************************************

-- 고객정보를 가져오는 SELECT 절 상관 서브쿼리
-- 조인으로 해결하는 것이 좋은 SQL
select	T1.CUS_ID
		,TO_CHAR(T1.ORD_DT, 'YYYYMMDD') as ORD_YMD
		-- 불필요하게 M_CUS_를 두 번이나 접근
		-- 성능에서 손해 볼 가능성이 큼
		,(select A.CUS_NM from M_CUS A where A.CUS_ID = T1.CUS_ID limit 1) as CUS_NM
		,(select A.CUS_GD from M_CUS A where A.CUS_ID = T1.CUS_ID limit 1) as CUS_GD
		,T1.ORD_AMT
from 	T_ORD T1
where	T1.ORD_DT >= TO_DATE('2017-08-01', 'YYYY-MM-DD')
and		T1.ORD_DT < TO_DATE('2017-09-01', 'YYYY-MM-DD');


-- ************************************************
-- PART I - 4.1.3 SQL3
-- ************************************************

-- 인라인-뷰 안에서 SELECT 절 서브쿼리를 사용한 예
-- SELECT 절의 서브쿼리는 가장 바깥의 SELECT 절에만 사용하도록 노력해야 함
select	T1.CUS_ID
		,SUBSTRING(T1.ORD_YMD from 1 for 6) as ORD_YM
		,MAX(T1.CUS_NM) as CUS_NM
		,MAX(T1.CUS_GD) as CUS_GD
		,T1.ORD_ST_NM
		,T1.PAY_TP_NM
		,SUM(T1.ORD_AMT) as ORD_AMT
from	(
			select	T1.CUS_ID
					,TO_CHAR(T1.ORD_DT, 'YYYYMMDD') as ORD_YMD
					,T2.CUS_NM
					,T2.CUS_GD
					,(select A.BAS_CD_NM
					  from		C_BAS_CD A
					  where		A.BAS_CD_DV = 'ORD_ST'
					  and		A.BAS_CD = T1.ORD_ST
					  and		A.LNG_CD = 'KO'
					  limit 1) as ORD_ST_NM
					,(select A.BAS_CD_NM
					  from		C_BAS_CD A
					  where		A.BAS_CD_DV = 'PAY_TP'
					  and		A.BAS_CD = T1.PAY_TP
					  and		A.LNG_CD = 'KO'
					  limit 1) as PAY_TP_NM
					 ,T1.ORD_AMT
			from	T_ORD T1
					,M_CUS T2
			where	T1.ORD_DT >= TO_DATE('2017-08-01', 'YYYY-MM-DD')
			and		T1.ORD_DT < TO_DATE('2017-09-01', 'YYYY-MM-DD')
			and		T1.CUS_ID = T2.CUS_ID
		) T1
group by T1.CUS_ID
		,SUBSTRING(T1.ORD_YMD from 1 for 6)
		,T1.ORD_ST_NM
		,T1.PAY_TP_NM;


-- ************************************************
-- PART I - 4.1.3 SQL4
-- ************************************************

-- 서브쿼리 안에서 조인을 사용한 예
-- 기본적으로 상관 서브쿼리는 메인 SQL의 결과 건수만큼 '반복' 수행됨
-- '과유불급'
select	T1.ORD_DT
		,T2.ORD_QTY
		,T2.ITM_ID
		,T3.ITM_NM
		,(	select	SUM(B.EVL_PT) / COUNT(*)
			from	M_ITM A
					,T_ITM_EVL B
			where	A.ITM_TP = T3.ITM_TP
			and		B.ITM_ID = A.ITM_ID
			and		B.EVL_DT < T1.ORD_DT
		  ) as ITM_TP_EVL_PT_AVG
from	T_ORD T1
		,T_ORD_DET T2
		,M_ITM T3
where	T1.ORD_DT >= TO_DATE('2017-08-01', 'YYYY-MM-DD')
and		T1.ORD_DT < TO_DATE('2017-09-01', 'YYYY-MM-DD')
and		T3.ITM_ID = T2.itm_id
and		T1.ORD_SEQ = T2.ord_seq
order by T1.ORD_DT, T2.ITM_ID;

4.1.4 SELECT 절 서브쿼리 - 단일 값

SELECT 절의 서브쿼리가 두 건 이상의 결과를 내보내거나 두 개 이 컬럼 이상의 결과를 내보내면 안 된다.

-- ************************************************
-- PART I - 4.1.4 SQL1
-- ************************************************

-- 실행이 불가능한 SELECT 절의 서브쿼리
--SELECT 절의 서브쿼리에서 두 컬럼을 지정.
select	T1.ORD_DT
		,T1.CUS_ID
		,(select	A.CUS_NM ,A.CUS_GD
		  from		M_CUS A
		  where		A.CUS_ID = T1.CUS_ID
		  )	as CUS_NM_GC
from	T_ORD T1
where	T1.ORD_DT >= TO_DATE('2017-04-01', 'YYYY-MM-DD')
and		T1.ORD_DT < TO_DATE('2017-05-01', 'YYYY-MM-DD');

--SELECT 절의 서브쿼리에서 두 건 이상의 데이터가 나오는 경우.
select	T1.ORD_DT
		,T1.CUS_ID
		,(select	A.ITM_ID
		  from		T_ORD_DET A
		  where		A.ORD_SEQ = T1.ORD_SEQ
		  ) as ITM_LIST
from	T_ORD T1
where	T1.ORD_DT >= TO_DATE('2017-04-01', 'YYYY-MM-DD')
and		T1.ORD_DT < TO_DATE('2017-05-01', 'YYYY-MM-DD');


-- ************************************************
-- PART I - 4.1.4 SQL2
-- ************************************************

-- 고객 이름과 등급을 합쳐서 하나의 컬럼으로 처리
-- 단가(UNT_PRC)와 주문수량(ORD_QTY)를 곱해서 주문금액으로 처리.
select	T1.ORD_DT
		,T1.CUS_ID
		,(select	A.CUS_NM || '(' || A.CUS_GD || ')'
		  from		M_CUS A
		  where 	A.CUS_ID = T1.CUS_ID
		  ) as CUS_NM_GD
		,(select	SUM(A.UNT_PRC * A.ORD_QTY)
		  from		T_ORD_DET A
		  where		A.ORD_SEQ = T1.ORD_SEQ
		  ) as ORD_AMT
from	T_ORD T1
where	T1.ORD_DT >= TO_DATE('2017-04-01', 'YYYY-MM-DD')
and		T1.ORD_DT < TO_DATE('2017-05-01', 'YYYY-MM-DD');


-- ************************************************
-- PART I - 4.1.4 SQL3
-- ************************************************

-- 고객별 마지막 ORD_SEQ의 주문금액
select	T1.CUS_ID
		,T1.CUS_NM
		,(select	cast(SUBSTRING(MAX(
								   LPAD(cast(A.ORD_SEQ as TEXT), 8, '0') || CAST(A.ORD_AMT as TEXT)
							   ), 9) as numeric)
		  from		T_ORD A
		  where		A.CUS_ID = T1.CUS_ID
		) as LAST_ORD_AMT
from	M_CUS T1
order by T1.CUS_ID;


-- ************************************************
-- PART I - 4.1.4 SQL4
-- ************************************************

-- 고객별 마지막 ORD_SEQ의 주문금액 – 중첩된 서브쿼리
-- SELECT 절의 서브쿼리는 조회되는 데이터 건수만큼 반복 실행됨
-- 조회되는 결과 건수가 작을 때만 사용
select	T1.CUS_ID
		,T1.CUS_NM
		,(
			select	B.ORD_AMT
			from 	T_ORD B
			where	B.ORD_SEQ =
						(select MAX(A.ORD_SEQ) from T_ORD A where A.CUS_ID = T1.CUS_ID)
		) as LAST_ORD_AMT
from 	M_CUS T1
order by T1.CUS_ID;


-- ************************************************
-- PART I - 4.1.4 SQL5
-- ************************************************

-- 잠재적인 오류가 존재하는 서브쿼리 – 정상 실행
select	T1.ORD_DT
		,T1.CUS_ID
		,(select	A.ORD_QTY
		  from		T_ORD_DET A
		  where 	A.ORD_sEQ = T1.ORD_SEQ
		  limit 1) as ORD_AMT
from	T_ORD T1
where	T1.ORD_SEQ = 2015;


-- ************************************************
-- PART I - 4.1.4 SQL6
-- ************************************************

-- 잠재적인 오류가 존재하는 서브쿼리 – 오류 발생
--1. 오류가 발생하는 서브쿼리(ORD_SEQ =2015)
select	T1.ORD_DT
		,T1.CUS_ID
		,(select	A.ORD_QTY
		  from		T_ORD_DET A
		  where		A.ORD_SEQ = T1.ORD_SEQ
		  ) as ORD_AMT
from	T_ORD T1
where 	T1.ORD_SEQ = 2015;

--2. T_ORD_DET에 ORD_SEQ가 2015인 데이터는 두 건이 존재한다.
select	T1.*
from	T_ORD_DET T1
where 	T1.ORD_SEQ = 2015;

4.1.5 WHERE 절 단독 서브쿼리

-- ************************************************
-- PART I - 4.1.5 SQL1
-- ************************************************

-- 마지막 주문 한 건을 조회하는 SQL, ORD_SEQ가 가장 큰 데이터가 마지막 주문이다.
select	*
from	T_ORD T1
where	T1.ORD_SEQ = (select MAX(A.ORD_SEQ) from T_ORD A);


-- ************************************************
-- PART I - 4.1.5 SQL2
-- ************************************************

-- 마지막 주문 한 건을 조회하는 SQL, ORDER BY와 ROWNUM을 사용
-- LIMIT : 단순한 행 제한
-- ROW_NUMBER() : 복잡한 필터링이나 조건에 따른 순위
-- ORD_SEQ에 대한 인덱스 필수
select	*
from	T_ORD T1
order by T1.ORD_SEQ desc
limit 1;

with RankedOrders as (
	select T1.*, ROW_NUMBER() over (order by T1.ORD_SEQ DESC) as ROW_NUM
	from T_ORD T1
)
select *
from RankedOrders
where ROW_NUM = 1;


-- ************************************************
-- PART I - 4.1.5 SQL3
-- ************************************************

-- 마지막 주문 일자의 데이터를 가져오는 SQL
select	*
from	T_ORD T1
where 	T1.ORD_DT = (select MAX(A.ORD_DT) from T_ORD A);


-- ************************************************
-- PART I - 4.1.5 SQL4
-- ************************************************

-- 3월 주문 건수가 4건 이상인 고객의 3월달 주문 리스트
select	*
from	T_ORD T1
where	T1.ORD_DT >= '2017-03-01'::DATE
and 	T1.ORD_DT < '2017-04-01'::DATE
and 	T1.CUS_ID in (
			select	A.CUS_ID
			from 	T_ORD A
			where 	A.ORD_DT >= '2017-03-01'::DATE
			and 	A.ORD_DT < '2017-04-01'::DATE
			group by A.cus_id
			having COUNT(*) >= 4
			);


-- ************************************************
-- PART I - 4.1.5 SQL5
-- ************************************************

-- 3월 주문 건수가 4건 이상인 고객의 3월달 주문 리스트 – 조인으로 처리
select 	T1.*
from	T_ORD T1
join	(
			select	A.CUS_ID
			from	T_ORD A
			where 	A.ORD_DT >= '2017-03-01'::DATE
			and 	A.ORD_DT < '2017-04-01'::DATE
			group by A.cus_id
			having COUNT(*) >= 4
		) T2
on 		T1.CUS_ID = T2.cus_id
where	T1.ORD_DT >= '2017-03-01'::DATE
and		T1.ORD_DT < '2017-04-01'::DATE;

4.1.6 WHERE 절 상관 서브쿼리

데이터의 존재 여부를 파악할 때 자주 사용한다.

특정 일자나 특정 월에 주문이 존재하는 고객 리스트를 뽑을 때 유용하다.

-- ************************************************
-- PART I - 4.1.6 SQL1
-- ************************************************

-- 3월에 주문이 존재하는 고객들을 조회
-- 반대는 NOT EXISTS
-- 다른 테이블에 데이터 존재 여부를 파악할 때 유용
select 	*
from 	M_CUS T1
where 	EXISTS(
			  select	1
			  from		T_ORD A
			  where		A.CUS_ID = T1.cus_id
			  and		A.ORD_DT >= '2017-03-01'::DATE
			  and 		A.ORD_DT < '2017-04-01'::DATE
			  );


-- ************************************************
-- PART I - 4.1.6 SQL2
-- ************************************************

-- 3월에 ELEC 아이템유형의 주문이 존재하는 고객들을 조회
select	*
from	M_CUS T1
where 	EXISTS(
			select	1
			from	T_ORD A
			join	T_ORD_DET B on A.ORD_SEQ = B.ord_seq
			join	M_ITM C on B.ITM_ID = C.itm_id 
			and		A.ORD_DT >= '2017-03-01'::DATE
			and 	A.ORD_DT < '2017-04-01'::DATE
			and 	C.ITM_TP = 'ELEC'
			);


-- ************************************************
-- PART I - 4.1.6 SQL3
-- ************************************************

-- 전체 고객을 조회, 3월에 주문이 존재하는지 여부를 같이 보여줌
select	T1.CUS_ID, T1.CUS_NM,
		(case when exists(
				select	1
				from	T_ORD A
				where	A.CUS_ID = T1.CUS_ID
				and		A.ORD_DT >= '2017-03-01'::DATE
				and		A.ORD_DT < '2017-04-01'::DATE
			)
			then 'Y'
			else 'N'
		end) as ORD_YN_03
from	M_CUS T1;
728x90