서브쿼리가 두세 단계로 중첩되기 시작하면, 쿼리를 읽는 순서가 안에서 바깥으로 거꾸로 가면서 머리가 아파집니다. CTE(Common Table Expression)는 이 문제를 풉니다. WITH 절로 중간 결과에 이름을 붙여 미리 정의해 두고, 본 쿼리에서는 그 이름을 테이블처럼 부릅니다. 복잡한 한 덩어리를 위에서 아래로 읽히는 여러 단계로 쪼개주는 도구입니다.
서브쿼리와 무엇이 다른가
| 구분 | 중첩 서브쿼리 | CTE (WITH 절) |
|---|---|---|
| 위치 | 쿼리 안에 박혀 있음 | 쿼리 맨 위에 분리 |
| 읽는 순서 | 안쪽부터 거꾸로 | 위에서 아래로 |
| 재사용 | 같은 서브쿼리를 반복 작성 | 이름으로 여러 번 참조 |
| 이름 | 없음 | 의미 있는 이름 부여 |
결과 자체는 같지만, CTE는 "이 중간 결과가 무엇인지" 이름으로 설명해 주기 때문에 협업과 유지보수에서 차이가 큽니다.
기본 문법
WITH 이름 AS ( ... ) 로 정의하고, 그 아래 본 쿼리에서 이름을 그냥 씁니다.
SQL
WITH dept_avg AS (
SELECT dept, AVG(salary) AS avg_salary
FROM employee
GROUP BY dept
)
SELECT e.name, e.dept, e.salary, d.avg_salary
FROM employee e
JOIN dept_avg d ON e.dept = d.dept
WHERE e.salary `>` d.avg_salary;
dept_avg라는 임시 결과(부서별 평균 급여)를 먼저 만들고, 본 쿼리에서 그걸 일반 테이블처럼 조인합니다. "부서 평균보다 많이 받는 직원"이라는 의도가 쿼리 구조에 그대로 드러납니다. 같은 걸 서브쿼리로 쓰면 SELECT 안에 평균 계산이 두 번 들어가 지저분해집니다.
단계를 여러 개로 이어 쓰기
CTE는 쉼표로 여러 개를 연결해, 앞 단계 결과를 뒤 단계가 참조할 수 있습니다.
SQL
WITH high_paid AS (
SELECT * FROM employee WHERE salary `>` 5000
),
by_dept AS (
SELECT dept, COUNT(*) AS cnt
FROM high_paid
GROUP BY dept
)
SELECT * FROM by_dept WHERE cnt `>=` 3;
"고액 연봉자를 추리고 → 부서별로 세고 → 3명 이상인 부서만" 이라는 3단계 사고 흐름이 그대로 코드 순서가 됩니다. 한 덩어리 중첩 쿼리보다 디버깅도 쉽습니다. 중간 CTE 하나만 SELECT 해보면 그 단계 결과를 바로 확인할 수 있으니까요.
요점 정리
- CTE는
WITH로 중간 결과에 이름을 붙여 미리 정의하는 기능이다. - 중첩 서브쿼리를 위에서 아래로 읽히는 단계로 풀어 가독성을 높인다.
- 여러 CTE를 이어 복잡한 로직을 사고 흐름 순서대로 표현할 수 있다.
서브쿼리를 CTE로 바꿔보며 같은 결과가 얼마나 읽기 쉬워지는지 직접 실행하는 실습은 데이터베이스 트랙에서 회원가입 없이 무료로 할 수 있습니다.