Search

8-2. WHERE 절 서브쿼리

8-2-1. WHERE 절 서브쿼리

WHERE 절에도 서브쿼리를 사용할 수 있다. SELECT 절에서는 단일 값이 출력되는 스칼라 형태로 서브쿼리를 사용해야 했다. WHERE 절에서는 단일 값이 아닌 형태로도 서브쿼리를 사용할 수 있다.
아래는 IN 조건과 WHERE 절 서브쿼리를 사용해, 2022년 1월 1일에 주문이 있는 매장 정보를 출력하는 SQL이다.
-- [SQL-8-2-1-a] SELECT T1.ShopId ,T1.ShopNm FROM startdb.Shop T1 WHERE T1.ShopId IN ( SELECT X.ShopId FROM startdb.Ord X WHERE X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220102','%Y%m%d')); ShopId ShopNm ------ --------------- S003 Chicago-1st S007 San Antonio-1st S011 Austin-1st
SQL
복사
위 SQL에서 서브쿼리만 떼어내서 실행해보자. Ord에서 2022년 1월 1일에 주문이 존재하는 세 건의 ShopId가 출력된다. 여러 건의 데이터가 출력되므로 스칼라 서브쿼리가 아닌 것을 알 수 있다.
-- [SQL-8-2-1-b] SELECT X.ShopId FROM startdb.Ord X WHERE X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220102','%Y%m%d'); ShopId ------ S003 S007 S011
SQL
복사
원래 SQL의 메인쿼리에서 ShopId 조건을 IN으로 받았기 때문에, 위와 같이 여러 건의 값을 받아 낼 수가 있는 것이다. 아래와 같이 메인쿼리의 ShopId에 대한 조건을 같다(=)로 변경하면 SQL은 에러가 발생한다.
-- [SQL-8-2-1-c] SELECT T1.ShopId ,T1.ShopNm FROM startdb.Shop T1 WHERE T1.ShopId = ( SELECT X.ShopId FROM startdb.Ord X WHERE X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220102','%Y%m%d')); Error -------------------------------- Subquery returns more than 1 row
SQL
복사
WHERE 절에서 같다(=) 조건으로 서브쿼리를 사용하려면, SELECT 절 서브쿼리처럼 스칼라 형태로 단일 값을 돌려주어야 한다. 아래는 같다(=) 조건으로 WHERE 절 서브쿼리를 사용하는 SQL이다. ORDER BY 와 LIMIT 1을 사용해 ShopSize가 가장큰 ShopId를 Ord의 조건으로 공급하고 있다.
-- [SQL-8-2-1-d] SELECT T1.OrdNo ,T1.OrdDtm ,T1.ShopId FROM startdb.Ord T1 WHERE T1.OrdDtm >= STR_TO_DATE('20220201','%Y%m%d') AND T1.OrdDtm < STR_TO_DATE('20220301','%Y%m%d') AND T1.ShopId = (SELECT X.ShopId FROM startdb.Shop X ORDER BY X.ShopSize DESC LIMIT 1); OrdNo OrdDtm ShopId ----- ------------------- ------ 26268 2022-02-04 14:00:00 S099 26481 2022-02-06 14:00:00 S099 26545 2022-02-08 14:00:00 S099 26767 2022-02-13 14:00:00 S099 26849 2022-02-21 14:00:00 S099
SQL
복사

8.2.2 EXISTS

EXISTS는 WHERE 절의 서브쿼리와 자주 짝을 이루는 특수 조건자다. EXISTS는 서브쿼리내의 데이터 존재여부를 확인하는 특수 조건자다. 메인 쿼리의 레코드별로 EXISTS의 서브쿼리를 수행해 데이터가 한 건이라도 존재하면 레코드를 조회 결과에 포함시킬 수 있다.
아래 SQL을 살펴보자. 매장운영유형(ShopOperTp)이 대리점(DIST)이면서 의자수량(ChairQty)이 70 이상인 매장 리스트를 조회하고 있다.
-- [SQL-8-2-2-a] Shop 데이터 SELECT T1.ShopId ,T1.ShopNm FROM startdb.Shop T1 WHERE T1.ShopOperTp ='DIST' AND T1.ChairQty >= 70 ORDER BY T1.ShopId; ShopId ShopNm ------ ------------- S088 San Diego-5th S099 Denver-5th S198 Seattle-10th S199 Denver-10th
SQL
복사
위 SQL의 결과를 보면 네 개의 매장 데이터가 출력되었다. 이제 위 매장들 중에 2022년 1월 1일에 주문이 한 건이라도 존재하는 매장만 출력하도록 해보자. 아래와 같이 EXISTS 를 추가해 처리할 수 있다.
-- [SQL-8-2-2-b] 대리점(DIST)이면서, 의자가 70개 이상인 매장 중에 -- 22년 1월달 주문이 한 건이라도 존재하는 매장만 조회 SELECT T1.ShopId ,T1.ShopNm FROM startdb.Shop T1 WHERE T1.ShopOperTp ='DIST' AND T1.ChairQty >= 70 AND EXISTS( SELECT * FROM startdb.Ord X WHERE X.ShopId = T1.ShopId AND X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220201','%Y%m%d') ) ORDER BY T1.ShopId; ShopId ShopNm ------ ----------- S199 Denver-10th
SQL
복사
S199 매장만 2022년 1월 1일에 주문이 존재하기 때문에 결과로 출력되고 나머지는 출력되지 못한 것을 알 수 있다.
EXISTS의 부정으로 NOT EXISTS를 사용할 수 있다. NOT EXISTS는 서브쿼리에 해당하는 데이터가 한 건도 없어야만 조건을 만족한다. 서브쿼리에 해당하는 데이터가 한 건이라도 있으면 조건을 만족하지 않는다. 위에서 작성한 SQL의 EXISTS를 NOT EXISTS로 변경해보자. 2022년 1월 1일에 주문이 한 건도 없는 매장이 출력된다.
-- [SQL-8-2-2-c] SELECT T1.ShopId ,T1.ShopNm FROM startdb.Shop T1 WHERE T1.ShopOperTp ='DIST' AND T1.ChairQty >= 70 AND NOT EXISTS( SELECT * FROM startdb.Ord X WHERE X.ShopId = T1.ShopId AND X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220201','%Y%m%d') ) ORDER BY T1.ShopId; ShopId ShopNm ------ ------------- S088 San Diego-5th S099 Denver-5th S198 Seattle-10th
SQL
복사
앞에서 살펴봤던 IN 조건을 사용한 WHERE 절 서브쿼리는 EXISTS 서브쿼리로 변경할 수 있다. 메인쿼리의 T1.ShopId를 서브쿼리 안쪽의 조건으로 집어 넣으면서 IN 조건을 EXISTS로 변경해버리면 된다.
-- [SQL-8-2-2-d] IN 서브쿼리 SELECT T1.ShopId ,T1.ShopNm FROM startdb.Shop T1 WHERE T1.ShopId IN ( SELECT X.ShopId FROM startdb.Ord X WHERE X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220102','%Y%m%d')); -- [SQL-8-2-2-e] 같은 결과를 만드는 EXISTS 서브쿼리 -- IN 조건 컬럼을 서브쿼리 안으로 밀어 넣어서 연결 조건으로 사용 SELECT T1.ShopId ,T1.ShopNm FROM startdb.Shop T1 WHERE EXISTS -- > IN을 EXISTS로 변경(조건 컬럼은 서브쿼리 안에서 처리) ( SELECT X.ShopId FROM startdb.Ord X WHERE X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220102','%Y%m%d') AND X.ShopId = T1.ShopId -- > IN 조건 컬럼 이동 );
SQL
복사
반드시 그런건 아니지만, WHERE 절 서브쿼리는 EXISTS 로 처리하는 것이 성능에 유리할 가능성이 좀 더 높다. 정확한건 DBMS마다, SQL마다, 데이터 분포마다 다를 수 있으며, 실제 실행계획을 확인해야만 정확이 알 수 있다. 이 부분은 SQL 성능과 관련되므로 여기서 다룰 수는 없다. 다만 WHERE 절 서브쿼리는 EXISTS가 성능에 유리할 가능성이 좀 더 높다 정도만 알고 넘어가기 바란다.

8-2-3. EXISTS와 JOIN

WHERE 절의 EXISTS 서브쿼리는 조인으로 풀어볼 수도 있다. 아래 SQL들을 살펴보기 바란다.
-- [SQL-8-2-3-a] -- 23년 1월 1일 주문을 조회 -- EXISTS를 사용해 20220705에 가입한 회원의 주문만 필터 SELECT T1.OrdNo ,T1.OrdDtm, T1.MemberId ,T1.OrdAmt FROM startdb.Ord T1 WHERE T1.OrdDtm >= STR_TO_DATE('20230101','%Y%m%d') AND T1.OrdDtm < STR_TO_DATE('20230102','%Y%m%d') AND EXISTS( SELECT * FROM startdb.Member A WHERE A.MemberID = T1.MemberId AND A.JoinDtm = STR_TO_DATE('20220705','%Y%m%d')) ORDER BY T1.OrdNo; OrdNo OrdDtm MemberId OrdAmt ----- ------------------- -------- -------- 67282 2023-01-01 19:30:00 M9998 5000.000 67284 2023-01-01 20:30:00 M9998 4500.000 -- [SQL-8-2-3-b] -- 비슷한 결과를 만들기 위해 JOIN으로 처리 -- 조인을 사용하면 필터 조건으로 사용하던 회원 정보를 같이 출력해 줄 수 있다. SELECT T1.OrdNo ,T1.OrdDtm, T1.MemberId ,T1.OrdAmt ,T2.NickNm ,T2.MemberGd ,T2.JoinDtm FROM startdb.Ord T1 INNER JOIN startdb.Member T2 ON (T2.MemberId = T1.MemberId) WHERE T1.OrdDtm >= STR_TO_DATE('20230101','%Y%m%d') AND T1.OrdDtm < STR_TO_DATE('20230102','%Y%m%d') AND T2.JoinDtm = STR_TO_DATE('20220705','%Y%m%d') ORDER BY T1.OrdNo; OrdNo OrdDtm MemberId OrdAmt NickNm MemberGd JoinDtm ----- ------------------- -------- -------- ---------- -------- ------------------- 67282 2023-01-01 19:30:00 M9998 5000.000 Thunder199 PLAT 2022-07-05 00:00:00 67284 2023-01-01 20:30:00 M9998 4500.000 Thunder199 PLAT 2022-07-05 00:00:00
SQL
복사
EXISTS를 조인으로 변경해 같은 데이터를 출력했다. 조인을 사용하면 필터 조건인 Member의 정보도 같이 출력해 줄 수 있는 장점이 있다. 다만, EXISTS를 조인으로 바꿀때는 주의가 필요하다. 메인 쿼리에 있던 테이블과 EXISTS 에서 처리한 테이블 간의 관계 차수에 따라, 조인으로 변경하는 것이 적절하지 않을 수 있다. 아래 예를 살펴보자.
-- [SQL-8-2-3-c] -- 직영(DIST) 매장이면서 의자수가 70개 이상인 데이터 조회 -- EXISTS를 사용해 2022년 1월에 주문이 있는 매장만 필터 처리 SELECT T1.ShopId ,T1.ShopNm, T1.ChairQty FROM startdb.Shop T1 WHERE T1.ShopOperTp ='DIST' AND T1.ChairQty >= 70 AND EXISTS( SELECT * FROM startdb.Ord X WHERE X.ShopId = T1.ShopId AND X.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20220201','%Y%m%d') ) ORDER BY T1.ShopId; ShopId ShopNm ChairQty ------ ----------- -------- S199 Denver-10th 74 -- [SQL-8-2-3-d] -- 같은 결과를 만들기 위해 JOIN으로 처리, 하지만 메인 쿼리의 결과 건수가 늘어난다. -- 조인은 결과 건수를 변화시킬 수 있다는 점에 주의가 필요 SELECT T1.ShopId ,T1.ShopNm, T1.ChairQty FROM startdb.Shop T1 INNER JOIN startdb.Ord T2 ON (T2.ShopId = T1.ShopId) WHERE T1.ShopOperTp ='DIST' AND T1.ChairQty >= 70 AND T2.OrdDtm >= STR_TO_DATE('20220101','%Y%m%d') AND T2.OrdDtm < STR_TO_DATE('20220201','%Y%m%d') ORDER BY T1.ShopId; ShopId ShopNm ChairQty ------ ----------- -------- S199 Denver-10th 74 S199 Denver-10th 74 S199 Denver-10th 74 S199 Denver-10th 74 … 생략 …
SQL
복사

8-2-4. 서브쿼리 다양하게 활용하기

서브쿼리에는 집계함수와 HAVING 조건도 처리할 수 있다. 서브쿼리는 말 그대로 부가적인 또 하나의 쿼리이기 때문에, 일반적으로 SQL을 작성하면서 사용할 수 있는 모든 문법을 사용할 수 있다. 아래는 HAVING 조건과 집계함수를 사용해 주문 수량이 30 개 이상인 회원 리스트를 출력하고 있다.
-- [SQL-8-2-4-a] 주문이 한 건이라도 있는 매장 SELECT T1.ShopId ,T1.ShopNm, T1.ShopSize FROM startdb.Shop T1 WHERE T1.ShopOperTp ='FLAG' AND T1.ShopSize >= 60 AND EXISTS( SELECT * FROM startdb.Ord X WHERE X.ShopId = T1.ShopId AND X.OrdDtm >= STR_TO_DATE('20201201','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20210101','%Y%m%d') ) ORDER BY T1.ShopId; ShopId ShopNm ShopSize ------ ---------------- -------- S018 Seattle-1st 61 S020 Washington-1st 65 S021 New York-2nd 67 S022 Los Angeles-2nd 69 S026 Philadelphia-2nd 77 S027 San Antonio-2nd 79 S030 San Jose-2nd 85 S031 Austin-2nd 87 S035 Charlotte-2nd 95 -- [SQL-8-2-4-b] 서브쿼리 안에 집계함수 사용 -- 주문 건수가 30건 이상인 매장 -- EXISTS 서브쿼리에서 집계함수와 HAVING 사용 SELECT T1.ShopId ,T1.ShopNm, T1.ShopSize FROM startdb.Shop T1 WHERE T1.ShopOperTp ='FLAG' AND T1.ShopSize >= 60 AND EXISTS( SELECT 1 -- > HAVING 처리를 위해 SELECT 절에 1을 사 FROM startdb.Ord X WHERE X.ShopId = T1.ShopId AND X.OrdDtm >= STR_TO_DATE('20201201','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20210101','%Y%m%d') HAVING COUNT(*)>=30 -- > HAVING COUNT 조건 사용 ) ORDER BY T1.ShopId; ShopId ShopNm ShopSize ------ ------------ -------- S018 Seattle-1st 61 S021 New York-2nd 67
SQL
복사
아래와 같이 서브쿼리 안에서 조인도 얼마든지 가능하다. 이를 통해 복잡합 로직의 데이터를 좀 더 편하게 추출할 수 있다.
-- [SQL-8-2-4-c] -- 2020년 12월에 Bakery 상품 주문이 한 건이라도 존재하는 매장 조회 SELECT T1.ShopOperTp ,( SELECT MAX(X.BaseCdNm) FROM startdb.BaseCd X WHERE X.BaseCdDv = 'ShopOperTp' AND X.BaseCd = T1.ShopOperTp) ShopOperTp_NM ,COUNT(*) ShopCnt FROM startdb.Shop T1 WHERE EXISTS( SELECT * FROM startdb.Ord X INNER JOIN startdb.OrdDet Y ON (X.OrdNo = Y.OrdNo) INNER JOIN startdb.Item Z ON (Z.ItemId = Y.ItemId) WHERE X.ShopId = T1.ShopId AND X.OrdDtm >= STR_TO_DATE('20201201','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20210101','%Y%m%d') AND Z.ItemCat = 'BKR' ) GROUP BY T1.ShopOperTp ORDER BY T1.ShopOperTp;
SQL
복사
서브쿼리는 SQL 작성을 편하게 해주지만, 과도하게 사용하면 성능에 좋지 않을 수 있다. 가능하면 사용을 자제하는 것이 좋다.

8-2-5. WHERE 절 서브쿼리의 순환 조건

WHERE 절 서브쿼리의 조건이 다시 메인쿼리로 공급되도록 순환되는 경우가 있는데 매우 안 좋은 패턴이다. 성능에 문제 있을 가능성이 매우 높은 패턴 중에 하나다. 아래 SQL을 살펴보자.
-- [SQL-8-2-5-a] SELECT T1.ShopId ,T1.OrdNo FROM startdb.Ord T1 WHERE T1.OrdDtm >= STR_TO_DATE('20210101','%Y%m%d') AND T1.OrdDtm < STR_TO_DATE('20210201','%Y%m%d') AND T1.OrdNo = ( SELECT MAX(X.OrdNo) FROM startdb.Ord X WHERE X.OrdDtm >= STR_TO_DATE('20210101','%Y%m%d') AND X.OrdDtm < STR_TO_DATE('20210201','%Y%m%d') AND X.ShopId = T1.ShopId) ORDER BY T1.ShopId ASC;
SQL
복사
WHERE 절 서브쿼리는 메인쿼리의 T1.ShopId를 받아서 처리하고 있다. 그리고 서브쿼리에서 처리된 결과(MAX(X.OrdNo))를 다시 메인쿼리의 T1.OrdNo에 조건으로 공급하고 있다. 두 테이블이 서로 값을 주고 받으면서 출력될 데이터를 걸러내게 되므로 많은 부하가 발생하게 된다. (WHERE 절의 EXISTS 서브쿼리는 이와 같은 순환 패턴이 아니다.)
위 SQL이 어떤 결과를 추출하려는 것인지 곰곰히 생각해 보자. 조회 결과와 함께 SQL을 잘 살펴보기 바란다. 위 SQL은 주문 테이블에서 ShopId별로 2021년 1월의 마지막 OrdNo만 출력하는 SQL이다. 그러므로 아래와 같이 GROUP BY SQL로 간단하게 작성이 가능하다.
-- [SQL-8-2-5-b] SELECT T1.ShopId ,MAX(T1.OrdNo) LastOrdNo FROM startdb.Ord T1 WHERE T1.OrdDtm >= STR_TO_DATE('20210101','%Y%m%d') AND T1.OrdDtm < STR_TO_DATE('20210201','%Y%m%d') GROUP BY T1.ShopId;
SQL
복사
항상 WHERE 절의 서브쿼리와 조건을 주고 받는 형태가 되지 않도록 SQL을 작성하기 바란다. 하지만 테이블 구조상 불가피하게 이러한 패턴이 나오기도 한다. 이 경우에는 주변의 선배들과 이야기에 성능에 문제가 있지 않을지 고민해보기 바란다. 가능하면 이와 같은 패턴이 나오지 않도록 테이블 구조가 만들어져 있는 것이 가장 좋다.

BOOSTER QUIZ

BOOSTER QUIZ ERD

BOOSTER QUIZ 8-2-1

2022년 12월 24일에 한 번이라도 주문이 있었던 회원 수를 알려주세요.
대상 테이블: 회원(Member), 주문(Ord)
조회 조건: 2022년 12월 24일(OrdDtm)에 주문이 한 건이라도 있는 회원
조회 컬럼: MemberCnt
추가 조건
MemberCnt는 2022년 12월 24일에 주문이 한 번이라도 있었던 회원 수입니다.
MemberCnt --------- 2208
SQL
복사

BOOSTER QUIZ 8-2-2

2022년 12월 24일에 초코머핀 주문이 한 번이라도 있었던 회원을 회원등급별로 카운트(회원수)해주세요.
대상 테이블: 회원(Member), 주문(Ord), 주문상세(OrdDet)
조회 조건
2022년 12월 24일(OrdDtm)에 ItemId가 CMFR(초코머핀)인 주문이 한건이라도 있는 회원
조회 컬럼: MemberGd, MemberCnt
추가 조건
MemberGd별 GROUP BY 처리하시오.
MemberCnt는 MemberGd별 카운트입니다.(회원수)
MemberGd MemberCnt -------- --------- GOLD 87 PLAT 38 SILV 94
SQL
복사
Upper: 8. SUB QUERY