JOIN과 ON
JOIN은 두 테이블을 연결할 때 사용하고 ON은 어떤 조건으로 두 테이블을 연결할지 지정한다.
FROM CAR_RENTAL_COMPANY_CAR r
JOIN CAR_RENTAL_COMPANY_DISCOUNT_PLAN d
ON d.CAR_TYPE = r.CAR_TYPE
여기서는 두 테이블의 CAR_TYPE이 같은 행끼리 연결한다. ON 뒤에는 조건을 하나만 작성해야 하는 것이 아니다. AND나 OR를 이용해 여러 조건을 작성할 수 있다.
JOIN CAR_RENTAL_COMPANY_DISCOUNT_PLAN d
ON d.CAR_TYPE = r.CAR_TYPE
AND d.DURATION_TYPE = '30일 이상'
이 경우 CAR_TYPE이 같으면서 DURATION_TYPE이 30일 이상인 데이터만 조인된다. ON도 WHERE처럼 하나의 조건식이라고 생각하면 이해하기 쉽다.
JOIN과 WHERE는 무엇이 다를까
다음과 같은 쿼리가 있다고 해보자.
FROM CAR r
JOIN DISCOUNT_PLAN d
ON d.CAR_TYPE = r.CAR_TYPE
AND d.DURATION_TYPE = '30일 이상'
WHERE r.CAR_TYPE IN ('세단', 'SUV')
SQL에서 별도의 조인 종류를 명시하지 않고 JOIN이라고 작성하면 기본적으로 INNER JOIN을 의미한다. INNER JOIN은 두 테이블에서 조인 조건을 만족하는 데이터만 연결한다. 이때 ON은 어떤 행끼리 조인할지를 결정한다.
여기서는 두 테이블의 CAR_TYPE이 같고 할인 정책의 DURATION_TYPE이 30일 이상인 데이터만 연결한다. 반면 WHERE는 JOIN이 이루어진 후 조회할 데이터에 조건을 적용한다. 따라서 이 쿼리에서는 ON으로 자동차와 할인 정책을 연결하고 WHERE에서 그중 세단과 SUV만 남긴다.
SQL 실행 순서
SQL은 작성하는 순서와 논리적으로 처리되는 순서가 다르다.
SELECT
FROM
JOIN
ON
WHERE
GROUP BY
HAVING
ORDER BY
논리적인 처리 순서는 다음과 같다.
FROM
JOIN / ON
WHERE
GROUP BY
HAVING
SELECT
ORDER BY
1. 먼저 조회할 테이블을 결정하고 조인을 수행한다.
2. 그다음 WHERE에서 필요 없는 행을 제외하고 GROUP BY가 있다면 데이터를 그룹화한다.
3. HAVING으로 그룹 결과를 필터링한 뒤 SELECT에서 최종적으로 출력할 컬럼과 계산식을 결정한다.
4. 마지막으로 ORDER BY가 결과를 정렬한다.
이 실행 순서를 이해하면 select 절의 별칭을 어디에서 사용할 수 있는지도 이해하기 쉬워진다.
SELECT 별칭을 WHERE에서 사용할 수 없는 이유
SELECT DAILY_FEE * 30 AS FEE
FROM CAR
WHERE FEE >= 500000;
다음 쿼리는 사용할 수 없다.
FEE는 SELECT에서 만들어지는 별칭이다. 하지만 논리적 실행 순서에서 WHERE가 SELECT보다 먼저 처리되기 때문에 WHERE에서는 아직 FEE라는 별칭을 사용할 수 없다. 따라서 다음처럼 실제 계산식을 작성해야 한다.
SELECT DAILY_FEE * 30 AS FEE
FROM CAR
ORDER BY FEE DESC;
반대로 ORDER BY는 SELECT 이후에 처리되므로 별칭을 사용할 수 있다.
WHERE와 HAVING
SELECT CAR_TYPE, COUNT(*)
FROM CAR
GROUP BY CAR_TYPE
HAVING COUNT(*) >= 3;
WHERE는 조회할 행 자체에 조건을 걸 때 사용한다. HAVING은 일반적으로 GROUP BY로 묶인 결과에 조건을 걸 때 사용한다.
SELECT DAILY_FEE * 30 AS FEE
FROM CAR
WHERE CAR_TYPE = 'SUV'
HAVING FEE >= 500000;
다만 GROUP BY가 없어도 HAVING을 사용할 수 있다. 따라서 WHERE 뒤에 바로 HAVING이 오는 것도 가능하다.
위 쿼리에서는 WHERE에서 먼저 SUV 차량만 필터링하고 HAVING에서 계산된 FEE를 기준으로 다시 필터링한다. HAVING을 반드시 GROUP BY와 함께 사용해야 하는 것은 아니다. 다만 본래 주된 용도는 그룹화된 결과나 집계 결과를 필터링하는 것이다.
MySQL과 Oracle의 HAVING 별칭 차이
여기서 MySQL과 Oracle에 차이가 있다. 다음처럼 SELECT에서 계산한 값에 FEE라는 별칭을 붙였다고 해보자.
SELECT DAILY_FEE * 30 AS FEE
FROM CAR
MySQL에서는 다음과 같이 HAVING에서 FEE를 사용할 수 있다.
SELECT DAILY_FEE * 30 AS FEE
FROM CAR
HAVING FEE >= 500000;
논리적 실행 순서상 HAVING이 SELECT보다 먼저인데도 가능한 이유는 MySQL이 HAVING에서 SELECT 별칭을 참조하는 것을 지원하기 때문이다. 실행 순서가 바뀌는 것은 아니다.
A select_expr can be given an alias using AS alias_name. The alias is used as the expression's column name and can be used in GROUP BY, ORDER BY, or HAVING lauses.
MySQL 공식 문서에서도 SELECT에서 정의한 별칭을 GROUP BY, ORDER BY, HAVING에서 사용할 수 있다고 설명한다.
Specify an alias for the column expression. Oracle Database will use this alias in the column heading of the result set. The AS keyword is optional. The alias effectively renames the select list item for the duration of the query. The alias can be used in the order_by_clause but not other clauses in the query.
Oracle 공식 문서에서는 SELECT에서 만든 컬럼 별칭을 ORDER BY에서는 사용할 수 있지만 같은 쿼리의 다른 절에서는 사용할 수 없다고 설명한다. 따라서 Oracle에서는 HAVING에서 SELECT 별칭을 직접 참조할 수 없기 때문에 계산식을 다시 작성해야 한다.
서브쿼리와 NOT IN
이번 문제의 조건 중 하나는 2022년 11월 1일부터 11월 30일까지 대여 가능한 차량만 조회하는 것이다. 대여 가능한 차량을 직접 찾기보다 먼저 11월과 대여 기간이 겹치는 차량을 찾은 뒤 제외하는 방식으로 접근했다. 11월과 대여 기간이 겹치는 차량은 다음 조건으로 찾을 수 있다.
대여 시작일이 11월 30일 이전이고 대여 종료일이 11월 1일 이후라면 11월과 대여 기간이 하루라도 겹친다. 이 차량들은 11월 동안 대여할 수 없으므로 메인 쿼리에서 NOT IN으로 제외한다.
WHERE r.CAR_ID NOT IN (
SELECT h.CAR_ID
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY h
WHERE h.START_DATE <= '2022-11-30'
AND h.END_DATE >= '2022-11-01'
)
IN은 괄호 안의 값 중 하나와 일치하는 데이터를 찾을 때 사용한다.
WHERE CAR_TYPE IN ('세단', 'SUV')
이는 다음 조건과 같다.
WHERE CAR_TYPE = '세단'
OR CAR_TYPE = 'SUV'
NOT IN은 반대로 괄호 안에 포함된 값을 제외한다. 따라서 이번 문제에서는 11월과 대여 기간이 겹치는 차량 ID를 서브쿼리로 구하고 NOT IN으로 제외해 11월 한 달 동안 대여 가능한 차량만 남겼다.
참고
https://school.programmers.co.kr/learn/courses/30/lessons/157339?language=oracle
프로그래머스
SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html
SQL Language Reference
docs.oracle.com
https://dev.mysql.com/doc/refman/8.4/en/select.html?ff=nopfpls
MySQL :: MySQL 8.4 Reference Manual :: 15.2.13 SELECT Statement
15.2.13 SELECT Statement SELECT [ALL | DISTINCT | DISTINCTROW ] [HIGH_PRIORITY] [STRAIGHT_JOIN] [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT] [SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS] select_expr [, select_expr] ... [into_option] [FROM table_referenc
dev.mysql.com