MSSQL 2.4 · 2부. 조회 심화

서브쿼리

스칼라·다중 행·상관 서브쿼리와 EXISTS 입니다. 1.9 에서 미뤄 둔 NOT IN 과 NOT EXISTS 를 여기서 마무리합니다.

예상 학습 시간 20분 난이도 중급
개념 설명

문장 안의 문장

서브쿼리는 다른 문장 안에 괄호로 넣은 SELECT 입니다. 조건에 사용할 값을 미리 알 수 없을 때 사용합니다.

"평균보다 조회수가 높은 글" 을 찾는다고 해 봅시다. 평균이 얼마인지는 조회해 봐야 압니다. 두 번 나눠 실행하는 대신 한 문장에 담습니다.

SQL
SELECT COUNT(*) AS 평균초과
FROM Board.POSTS
WHERE hit_count > (SELECT AVG(hit_count) FROM Board.POSTS);
결과
평균초과
214
(1개 행이 영향을 받음)

507건 가운데 214건이 평균(194)을 넘었습니다. 안쪽 문장이 값 하나를 내고, 바깥이 그 값을 조건으로 사용했습니다. 이렇게 값 하나를 내는 서브쿼리를 스칼라 서브쿼리라고 합니다.

상세 사용법

값이 하나여야 합니다

> 같은 연산자 뒤에 오는 서브쿼리가 두 행 이상 을 내면 오류입니다. 어느 값과 비교해야 할지 알 수 없기 때문입니다.

SQL
SELECT TOP 3 num, title FROM Board.POSTS
WHERE hit_count > (SELECT hit_count FROM Board.POSTS WHERE board_num = 1);
오류
메시지 512 Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.

이 오류는 자료에 따라 나기도 하고 안 나기도 합니다. 지금은 공지사항이 35건이라 났지만, 만약 1건뿐이었다면 통과했을 것입니다. 그러다 글이 하나 더 늘어난 날 갑자기 오류가 발생합니다. 값 하나가 확실하지 않다면 MAX 로 감싸거나 IN 을 사용하십시오.

최소 예제

여러 값과 비교하기 — IN

IN 뒤에는 여러 행이 와도 됩니다. 그 가운데 하나와 같으면 참입니다.

SQL
SELECT COUNT(*) AS 말머리있는게시판글
FROM Board.POSTS
WHERE board_num IN (
    SELECT num FROM Board.BOARDS WHERE use_category = 1
);
결과
말머리있는게시판글
472
(1개 행이 영향을 받음)

게시판 번호를 직접 적지 않고 설정으로 찾아냈습니다. 나중에 말머리를 사용하는 게시판이 늘어도 이 문장은 그대로 맞습니다.

상세 사용법

바깥 행을 참조하는 서브쿼리

서브쿼리 안에서 바깥 문장의 열을 참조할 수 있습니다. 그러면 바깥의 행마다 안쪽이 한 번씩 실행됩니다. 이것을 상관 서브쿼리라고 합니다.

SQL
SELECT TOP 5 P.num, P.title,
       (SELECT COUNT(*) FROM Board.COMMENTS AS C
         WHERE C.post_num = P.num) AS 댓글수
FROM Board.POSTS AS P
ORDER BY P.num;
결과
numtitle댓글수
1조인 질문드립니다1
2트랜잭션 질문드립니다2
3페이징 질문드립니다3
4백업 질문드립니다4
5실행 계획 질문드립니다0
(5개 행이 영향을 받음)

2.2 에서 LEFT JOINGROUP BY 로 했던 것과 같은 결과입니다. 이쪽이 읽기 쉽습니다. 묶을 필요가 없고, COUNT(*) 인지 COUNT(열) 인지 고민할 일도 없습니다.

다만 세는 열이 여럿이면 조인이 낫습니다. 댓글 수와 첨부 수를 함께 내려면 서브쿼리를 두 번 적어야 하고, 그만큼 안쪽 조회가 두 번 돕니다.

상세 사용법

있는지만 보기 — EXISTS

EXISTS행이 하나라도 있는지만 봅니다. 값을 가져오지 않으므로 안쪽에 무엇을 적든 상관없고, 관례로 SELECT 1 을 적습니다.

SQL
SELECT COUNT(*) AS 댓글있는글
FROM Board.POSTS AS P
WHERE EXISTS (SELECT 1 FROM Board.COMMENTS AS C WHERE C.post_num = P.num);

SELECT COUNT(*) AS 댓글없는글
FROM Board.POSTS AS P
WHERE NOT EXISTS (SELECT 1 FROM Board.COMMENTS AS C WHERE C.post_num = P.num);
결과
댓글있는글
406
결과
댓글없는글
101

406 + 101 = 507 로 전체와 맞습니다. EXISTS첫 행을 찾는 순간 멈춥니다. 몇 개인지 세지 않으므로 존재 여부만 필요할 때 COUNT(*) > 0 보다 낫습니다.

상세 사용법

NOT IN 을 다시 봅니다

1.9 에서 NOT IN 이 한 건도 내지 않는 것을 보았습니다. 그 까닭을 이제 서브쿼리로 정확히 적을 수 있습니다.

SQL
-- 목록에 NULL 이 섞여 있습니다
SELECT COUNT(*) AS not_in결과 FROM Member.USERS
WHERE user_id NOT IN (SELECT email FROM Member.USERS);

-- 안쪽에서 NULL 을 빼면
SELECT COUNT(*) AS not_in_널제외 FROM Member.USERS
WHERE user_id NOT IN (SELECT email FROM Member.USERS WHERE email IS NOT NULL);
결과
not_in결과
0
결과
not_in_널제외
20

고치는 방법이 둘입니다. 안쪽에서 NULL 을 빼거나, NOT EXISTS 로 바꾸는 것입니다. 뒤엣것을 권합니다. 조건이 늘어도 안전하고, 안쪽 열에 NULL 이 생겼는지 매번 신경 쓰지 않아도 됩니다.

사용하는 것NULL 에언제
IN안전합니다목록과 견줄 때
NOT IN위험합니다안쪽이 NOT NULL 일 때만
EXISTS안전합니다있는지만 볼 때
NOT EXISTS안전합니다없는 것을 찾을 때
상세 사용법

FROM 절에 두기 — 파생 테이블

서브쿼리를 FROM 에 두면 임시 테이블처럼 사용할 수 있습니다. 집계한 결과를 다시 집계할 때 필요합니다.

SQL
SELECT AVG(글수) AS 사람당평균글수, MAX(글수) AS 최다
FROM (
    SELECT U.num, COUNT(*) AS 글수
    FROM Board.POSTS AS P
        INNER JOIN Member.USERS AS U ON U.num = P.user_num
    GROUP BY U.num
) AS T;
결과
사람당평균글수최다
2544
(1개 행이 영향을 받음)

집계 함수를 겹쳐 적을 수는 없으므로(AVG(COUNT(*)) 는 오류입니다) 이렇게 두 단계로 나눕니다.

FROM 의 서브쿼리에는 반드시 이름을 붙여야 합니다 — 위의 AS T 입니다. 없으면 오류입니다.

중첩이 깊어지면 읽기 어려워집니다. 같은 일을 위에서 아래로 풀어 적는 방법이 있고, 그것이 다음 단원의 WITH 입니다(2.5).

상세 사용법

서브쿼리와 조인, 무엇을 쓸까

하려는 것알맞은 것
양쪽 열을 함께 내기조인. 서브쿼리로는 오른쪽 열을 여러 개 가져오기 번거롭습니다
있는지 없는지만EXISTS · NOT EXISTS
개수 하나만 덧붙이기상관 서브쿼리가 읽기 쉽습니다
집계한 것을 다시 집계파생 테이블 또는 CTE(2.5)

속도는 대개 비슷합니다. 데이터베이스가 문장을 그대로 실행하지 않고 같은 뜻의 더 빠른 방법으로 바꾸기 때문입니다. 그러니 읽기 쉬운 쪽을 선택하십시오. 정말 느린 자리는 5부에서 실행 계획을 보고 판단합니다.

실습 문제

직접 해보기

1. 글을 한 번도 작성하지 않은 회원 난이도 하

글을 하나도 작성하지 않은 회원이 있는지 찾아보세요.

SELECT U.num, U.nickname FROM Member.USERS AS U WHERE NOT EXISTS ( SELECT 1 FROM Board.POSTS AS P WHERE P.user_num = U.num ); -- 실습 데이터에서는 한 건도 나오지 않습니다. -- 20명 모두 글을 작성했기 때문입니다. -- NOT IN 으로 적어도 되지만, user_num 은 NOT NULL 이라 -- 이 경우는 안전합니다. 습관을 NOT EXISTS 로 두는 편이 낫습니다.
2. 자기 게시판 평균을 넘는 글 난이도 중

각 글의 조회수가 그 글이 속한 게시판의 평균보다 높은 것을 세어 보세요. 게시판마다 평균이 다릅니다.

안쪽 서브쿼리에서 바깥 행의 board_num 을 참조하면 됩니다. 상관 서브쿼리입니다.
SELECT COUNT(*) AS 게시판평균초과 FROM Board.POSTS AS P WHERE P.hit_count > ( SELECT AVG(X.hit_count) FROM Board.POSTS AS X WHERE X.board_num = P.board_num ); -- 안쪽의 X.board_num = P.board_num 이 핵심입니다. -- 이것이 없으면 전체 평균과 비교하게 됩니다. -- 윈도 함수(2.7)로 적으면 안쪽 조회를 반복하지 않습니다.
요약
  • 서브쿼리는 조건에 사용할 값을 미리 알 수 없을 때 사용합니다.
  • 비교 연산자 뒤의 서브쿼리는 값이 하나여야 합니다(오류 512). 자료가 늘면 갑자기 날 수 있습니다.
  • 바깥 행을 참조하면 상관 서브쿼리가 되어 행마다 한 번씩 실행됩니다.
  • EXISTS 는 첫 행을 찾는 순간 멈춥니다. 존재만 볼 때 COUNT 보다 낫습니다.
  • 없는 것을 찾을 때는 NOT EXISTS 입니다. NOT IN 은 NULL 에 무너집니다.
  • FROM 의 서브쿼리에는 이름을 붙여야 합니다.