서브쿼리
스칼라·다중 행·상관 서브쿼리와 EXISTS 입니다. 1.9 에서 미뤄 둔 NOT IN 과 NOT EXISTS 를 여기서 마무리합니다.
문장 안의 문장
서브쿼리는 다른 문장 안에 괄호로 넣은 SELECT
입니다. 조건에 사용할 값을 미리 알 수 없을 때 사용합니다.
"평균보다 조회수가 높은 글" 을 찾는다고 해 봅시다. 평균이 얼마인지는 조회해 봐야 압니다. 두 번 나눠 실행하는 대신 한 문장에 담습니다.
SELECT COUNT(*) AS 평균초과 FROM Board.POSTS WHERE hit_count > (SELECT AVG(hit_count) FROM Board.POSTS);
| 평균초과 |
|---|
| 214 |
507건 가운데 214건이 평균(194)을 넘었습니다. 안쪽 문장이 값 하나를 내고, 바깥이 그 값을 조건으로 사용했습니다. 이렇게 값 하나를 내는 서브쿼리를 스칼라 서브쿼리라고 합니다.
값이 하나여야 합니다
> 같은 연산자 뒤에 오는 서브쿼리가 두 행 이상
을 내면 오류입니다. 어느 값과 비교해야 할지 알 수 없기 때문입니다.
SELECT TOP 3 num, title FROM Board.POSTS WHERE hit_count > (SELECT hit_count FROM Board.POSTS WHERE board_num = 1);
이 오류는 자료에 따라 나기도 하고 안 나기도 합니다. 지금은 공지사항이
35건이라 났지만, 만약 1건뿐이었다면 통과했을 것입니다. 그러다 글이 하나 더 늘어난 날
갑자기 오류가 발생합니다. 값 하나가 확실하지 않다면 MAX 로 감싸거나
IN 을 사용하십시오.
여러 값과 비교하기 — IN
IN 뒤에는 여러 행이 와도 됩니다. 그 가운데 하나와 같으면
참입니다.
SELECT COUNT(*) AS 말머리있는게시판글 FROM Board.POSTS WHERE board_num IN ( SELECT num FROM Board.BOARDS WHERE use_category = 1 );
| 말머리있는게시판글 |
|---|
| 472 |
게시판 번호를 직접 적지 않고 설정으로 찾아냈습니다. 나중에 말머리를 사용하는 게시판이 늘어도 이 문장은 그대로 맞습니다.
바깥 행을 참조하는 서브쿼리
서브쿼리 안에서 바깥 문장의 열을 참조할 수 있습니다. 그러면 바깥의 행마다 안쪽이 한 번씩 실행됩니다. 이것을 상관 서브쿼리라고 합니다.
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;
| num | title | 댓글수 |
|---|---|---|
| 1 | 조인 질문드립니다 | 1 |
| 2 | 트랜잭션 질문드립니다 | 2 |
| 3 | 페이징 질문드립니다 | 3 |
| 4 | 백업 질문드립니다 | 4 |
| 5 | 실행 계획 질문드립니다 | 0 |
2.2 에서 LEFT JOIN 과 GROUP BY 로
했던 것과 같은 결과입니다. 이쪽이 읽기 쉽습니다. 묶을 필요가 없고,
COUNT(*) 인지 COUNT(열) 인지
고민할 일도 없습니다.
다만 세는 열이 여럿이면 조인이 낫습니다. 댓글 수와 첨부 수를 함께 내려면 서브쿼리를 두 번 적어야 하고, 그만큼 안쪽 조회가 두 번 돕니다.
있는지만 보기 — EXISTS
EXISTS 는 행이 하나라도 있는지만 봅니다.
값을 가져오지 않으므로 안쪽에 무엇을 적든 상관없고, 관례로
SELECT 1 을 적습니다.
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 이 한 건도 내지 않는 것을 보았습니다.
그 까닭을 이제 서브쿼리로 정확히 적을 수 있습니다.
-- 목록에 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 에 두면 임시 테이블처럼
사용할 수 있습니다. 집계한 결과를 다시 집계할 때 필요합니다.
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;
| 사람당평균글수 | 최다 |
|---|---|
| 25 | 44 |
집계 함수를 겹쳐 적을 수는 없으므로(AVG(COUNT(*)) 는
오류입니다) 이렇게 두 단계로 나눕니다.
FROM 의 서브쿼리에는 반드시 이름을 붙여야 합니다 —
위의 AS T 입니다. 없으면 오류입니다.
중첩이 깊어지면 읽기 어려워집니다. 같은 일을 위에서 아래로 풀어 적는 방법이
있고, 그것이 다음 단원의 WITH 입니다(2.5).
서브쿼리와 조인, 무엇을 쓸까
| 하려는 것 | 알맞은 것 |
|---|---|
| 양쪽 열을 함께 내기 | 조인. 서브쿼리로는 오른쪽 열을 여러 개 가져오기 번거롭습니다 |
| 있는지 없는지만 | EXISTS · NOT EXISTS |
| 개수 하나만 덧붙이기 | 상관 서브쿼리가 읽기 쉽습니다 |
| 집계한 것을 다시 집계 | 파생 테이블 또는 CTE(2.5) |
속도는 대개 비슷합니다. 데이터베이스가 문장을 그대로 실행하지 않고 같은 뜻의 더 빠른 방법으로 바꾸기 때문입니다. 그러니 읽기 쉬운 쪽을 선택하십시오. 정말 느린 자리는 5부에서 실행 계획을 보고 판단합니다.
직접 해보기
글을 하나도 작성하지 않은 회원이 있는지 찾아보세요.
각 글의 조회수가 그 글이 속한 게시판의 평균보다 높은 것을 세어 보세요. 게시판마다 평균이 다릅니다.
- 서브쿼리는 조건에 사용할 값을 미리 알 수 없을 때 사용합니다.
- 비교 연산자 뒤의 서브쿼리는 값이 하나여야 합니다(오류 512). 자료가 늘면 갑자기 날 수 있습니다.
- 바깥 행을 참조하면 상관 서브쿼리가 되어 행마다 한 번씩 실행됩니다.
EXISTS는 첫 행을 찾는 순간 멈춥니다. 존재만 볼 때COUNT보다 낫습니다.- 없는 것을 찾을 때는
NOT EXISTS입니다.NOT IN은 NULL 에 무너집니다. FROM의 서브쿼리에는 이름을 붙여야 합니다.