내일이면 금요일!
서브쿼리는 SELECT문 안에 또 다른 SELECT문이 들어가 있는 구조다.
바깥 쿼리(메인 쿼리)가 조건값으로 쓸 값을, 직접 하드코딩하지 않고 안쪽 쿼리(서브쿼리)가 그때그때 구해서 넘겨준다.
SELECT STUDENT_NAME
FROM tb_student S
WHERE ABSENCE_YN = 'Y'
AND DEPARTMENT_NO = (
SELECT DEPARTMENT_NO
FROM TB_DEPARTMENT
WHERE DEPARTMENT_NAME = '국어국문학과'
);
국어국문학과의 학과번호가 몇 번인지 몰라도, 저 자리에 학과 이름 조건을 넣은 서브쿼리를 두면 알아서 번호로 치환돼서 비교된다.
서브쿼리는 위치에 따라 부르는 이름이 다르다.
| 위치 | 이름 | 특징 |
|---|---|---|
| SELECT 절 | 스칼라 서브쿼리 | 결과가 값 하나(1행 1열)여야 함 |
| FROM 절 | 인라인 뷰(Inline View) | 서브쿼리 결과를 임시 테이블(가상 테이블, virtual table)처럼 취급, 별칭 필수 |
| WHERE 절 | (일반) 서브쿼리 | 조건값으로 사용 |
FROM절에 쓴 서브쿼리, 즉 인라인 뷰는 실행되는 그 순간에만 존재하는 가상 테이블이다.
어제 JOIN이 "가상의 테이블을 만들어서 결과를 뽑는 것"이라고 배웠는데, 오늘은 그 가상 테이블을 JOIN이 아니라 서브쿼리로도 직접 만들 수 있다는 걸 배웠다.
서브쿼리가 몇 행, 몇 열을 반환하는지에 따라 쓸 수 있는 연산자가 달라진다.
| 구분 | 단일열 | 다중열 |
|---|---|---|
| 단일행 | 일반 연산자(=, >, < 등) | (col1,col2) = (...) |
| 다중행 | IN, ANY, ALL | (col1,col2) IN (...) |
서브쿼리가 여러 행을 반환할 수 있는 자리에 =를 그대로 쓰면 "ERROR 1242 (21000): Subquery returns more than 1 row"라는 에러가 난다. 그리고 열을 하나만 비교하면 안 되는 경우도 있다. 서별 최소급여 사원을 찾을 때 급여 하나만 비교하면, 우연히 같은 급여를 받는 다른 부서 사원까지 걸릴 수 있어서 부서번호와 급여를 묶어서 비교해야 한다.
SELECT EMP_NAME, DEPT_ID, SALARY
FROM employee E
WHERE (DEPT_ID, SALARY) IN (
SELECT DEPT_ID, MIN(SALARY)
FROM employee
GROUP BY DEPT_ID
);
-- 급여합 1등 부서 찾기
SELECT V.DEPT_NAME, MAX(V.SUM)
FROM (
SELECT D.DEPT_NAME, SUM(E.SALARY) AS `SUM`
FROM department D
JOIN employee E ON (D.DEPT_ID = E.DEPT_ID)
GROUP BY DEPT_NAME
) V;
부서별 합계(SUM)가 전부 나와야 그중 최댓값(MAX)을 고를 수 있는데, MAX(SUM(...))처럼 한 자리에 겹쳐 쓰면 아직 다 만들어지지도 않은 값을 놓고 최댓값을 구하라는 순서가 돼서 에러가 난다.
그래서 부서별 합계를 인라인 뷰로 먼저 완성한 다음, 그 결과 위에서 다시 MAX를 구했다.
다중행 서브쿼리를 비교연산자와 같이 쓸 때 붙이는 지시자다.
| 연산자 | 의미 |
|---|---|
> ANY | 목록 중 어느 하나보다 크면 참 → 최솟값보다 크다 |
< ANY | 목록 중 어느 하나보다 작으면 참 → 최댓값보다 작다 |
> ALL | 목록 전부보다 커야 참 → 최댓값보다 크다 |
< ALL | 목록 전부보다 작아야 참 → 최솟값보다 작다 |
-- 과장 전원보다 급여가 적은 대리
SELECT E.EMP_NAME, E.SALARY
FROM employee E
JOIN job J ON (E.JOB_ID = J.JOB_ID)
WHERE J.JOB_TITLE = '대리'
AND SALARY < ALL (
SELECT E.SALARY FROM employee E
JOIN job J ON (E.JOB_ID = J.JOB_ID)
WHERE J.JOB_TITLE = '과장'
);
ANY는 조건을 만족하는 값이 목록에 하나만 있어도 참이 되고,
ALL은 목록에 있는 값 전부가 조건을 만족해야 참이 된다고 정리하면 헷갈리지 않는다.
= ANY는 IN과 같은 뜻이다.
SELECT D.DEPT_NAME, SUM(E.SALARY) AS S
FROM employee E
JOIN department D ON (E.DEPT_ID=D.DEPT_ID)
GROUP BY E.DEPT_ID
ORDER BY S DESC
LIMIT 1;
-- OFFSET: M+1번지부터 N번지까지 범위 검색
SELECT E.DEPT_ID, SUM(SALARY)
FROM employee E
GROUP BY DEPT_ID
ORDER BY 2 DESC
LIMIT 3 OFFSET 0;
2.4에서 서브쿼리로 복잡하게 풀었던 "급여합 1등 부서 찾기"를, 정렬 후 LIMIT으로 자르면 훨씬 짧게 끝난다.
다만 급여합이 똑같은 부서가 두 곳이어도 LIMIT 1은 딱 한 행만 돌려준다는 차이는 있다.
SELECT E.EMP_NAME, J.JOB_TITLE, E.SALARY
FROM job J
JOIN employee E ON (J.JOB_ID = E.JOB_ID)
WHERE E.SALARY = (
SELECT TRUNCATE(AVG(SALARY), -5)
FROM employee EE
WHERE EE.JOB_ID = E.JOB_ID
);
서브쿼리 조건절에 등장하는 E.JOB_ID는 이 서브쿼리 자신의 것이 아니라, 바깥 FROM절에서 가져온 값이다. 이렇게 안쪽 쿼리가 자기 힘만으로 완성되지 못하고 바깥 쿼리의 값을 매번 받아와야 하는 구조를 상관관계 서브쿼리라고 부른다. 그래서 실행 방식도 다르다. 바깥 쿼리가 한 행씩 넘어갈 때마다 안쪽 서브쿼리가 그 행 기준으로 다시 계산된다. 같은 문제를 직급별 평균을 인라인 뷰로 먼저 구해두고 그 결과와 조인하는 방식으로도 풀 수 있는데, 이 경우엔 서브쿼리가 한 번만 실행된다는 점이 다르다.
CREATE TABLE TB_BLOG(
BLOG_ID VARCHAR(50),
TITLE VARCHAR(100),
CONTENT VARCHAR(100),
EMAIL VARCHAR(100),
CATEGORY VARCHAR(100) CHECK(CATEGORY IN ('전체','취미','일상','코딩')),
CREATE_AT DATE DEFAULT SYSDATE(),
PRIMARY KEY (BLOG_ID)
);
DDL은 테이블, 뷰 같은 객체의 구조 자체를 정의하는 언어로 CREATE(생성), ALTER(변경), DROP(삭제)이 있다.
오늘 다룬 제약조건은 PRIMARY KEY(NOT NULL+UNIQUE 성질을 동시에 가짐), FOREIGN KEY, NOT NULL, UNIQUE, CHECK 총 다섯 가지다. 컬럼을 선언하면서 바로 옆에 붙이면 컬럼 레벨 제약, 모든 컬럼을 다 쓴 뒤 따로 한 줄로 적으면 테이블 레벨 제약이라고 부른다. DEFAULT는 INSERT할 때 값을 안 넣으면 대신 채워지는 값을 미리 정해두는 것이다.
직접 해본 것:
워크북 18번(국어국문학과 총평점 최고 학생)을 서브쿼리로 풀었다.
SELECT S.STUDENT_NO, S.STUDENT_NAME
FROM tb_student S
JOIN tb_grade G ON (S.STUDENT_NO = G.STUDENT_NO)
WHERE S.DEPARTMENT_NO = (
SELECT DEPARTMENT_NO FROM tb_department WHERE DEPARTMENT_NAME = '국어국문학과'
)
GROUP BY S.STUDENT_NO, S.STUDENT_NAME
HAVING AVG(G.POINT) = (
SELECT MAX(V.AVG)
FROM (
SELECT AVG(G.POINT) AS `AVG`
FROM tb_student S
JOIN tb_grade G ON (S.STUDENT_NO = G.STUDENT_NO)
WHERE S.DEPARTMENT_NO = (
SELECT DEPARTMENT_NO FROM tb_department WHERE DEPARTMENT_NAME = '국어국문학과'
)
GROUP BY S.STUDENT_NO
) V
);
TABLEDB에 TB_BLOG 테이블을 만들어서 PRIMARY KEY, DEFAULT, CHECK 제약조건을 하나씩 걸어보고, 정상적으로 들어가는 INSERT와 거부되는 INSERT를 직접 비교해봤다.
① 집계함수를 중첩해서 쓴 오류
부서별 급여 총합 중 최댓값을 구하려고 MAX(SUM(E.SALARY))를 바로 썼다.
-- 원인이 된 코드
SELECT D.DEPT_NAME, MAX(SUM(E.SALARY))
FROM department D
JOIN employee E ON (D.DEPT_ID = E.DEPT_ID)
GROUP BY DEPT_NAME;
집계함수 위에 집계함수를 바로 씌울 수 없어서 에러가 났다. 부서별 합계를 인라인 뷰로 먼저 만들고, 그 결과에서 다시 MAX를 구하는 방식으로 해결했다.
② 집계함수와 전체 컬럼(*)을 같이 쓴 오류
-- 원인이 된 코드
SELECT E.*, MIN(SALARY)
FROM employee E;
E.*(모든 일반 컬럼)와 MIN(SALARY)(집계함수)를 GROUP BY 없이 같이 써서 에러가 났다.
집계함수와 일반 컬럼을 같이 쓰려면 GROUP BY로 묶어야 한다는 걸 다시 확인했다.
#LGCNS #LGCNS6기 #개발자 #LGCNSINSPIRECAMP