똑같이 프로그래머스 LEVEL-4 문제인데 상당히 어렵네요..ㅠㅠ 엄청나게 틀리고 수정하는 작업을 반복했습니다.
제 기준으로, 중요한 개념 정리가 여러개 나오는데, 강조 표시 해둘게요..!!!
마지막 3. 마무리 부분에서 나온 것처럼, '개념 설명들' 및 '정답 코드만 분석'하시면 시간을 아끼실 수 있습니다!




총 3개의 테이블이 있습니다.
1.회사에서 대여 중인 자동차 정보 담은 테이블(CAR_RENTAL_COMPANY_CAR),
2.자동차 대여 기록 정보를 담은 테이블(CAR_RENTAL_COMPANY_RENTAL_HISTORY),
3.자동차 종류 별 대여 기간 종류 별 할인 정책 정보 담은 테이블(CAR_RENTAL_COMPANY_DISCOUNT_PLAN)
그리고 각각의 테이블에는 이렇게 열들이 구성되어 있습니다.
1.CAR_RENTAL_COMPANY_CAR(CAR_ID, CAR_TYPE, DAILY_FEE, OPTIONS)
->자동차 종류: '세단', 'SUV', '승합차', '트럭', '리무진'
->옵션 리스트: '주차감지센서', '스마트키', '네비게이션', '통풍시트', '열선시트', '후방카메라', '가죽시트'2.CAR_RENTAL_COMPANY_RENTAL_HISTORY(HISTORY_ID, CAR_ID, START_DATE, END_DATE)
3.CAR_RENTAL_COMPANY_DISCOUNT_PLAN(PLAN_ID, CAR_TYPE, DURATION_TYPE, DISCOUNT_RATE)
->대여 기간 종류: '7일 이상', '30일 이상', '90일 이상'
우리가 구해야하는 문제는 다음과 같습니다.
※문제: 3개의 테이블에서 1. 자동차 종류: SUV,세단 2. 2022-11-1 ~ 2022-11-30 대여 가능 3. 대여 금액: 50~200
-> 자동차 ID, 자동차 종류, 대여 금액(FEE) 리스트 출력하라.
-> 대여 금액 기준 DESC, 대여 금액이 같으면 자동차 종류 기준 ASC, 자동차 종류까지 같으면 자동차 ID 기준 DESC
요구하는 게 참 많네요 ;; 😂😂😂
일단, 본격적으로 문제를 풀기에 앞서, 3개의 테이블의 관계를 찾아보니, 제 눈에 보이는 건 2번째 테이블이 1번째 테이블을 일반 참조(FK)하는 모습이네요.
그리고 아마, CAR_ID,HISTORY_ID,PLAN_ID가 각각의 테이블의 PK이겠죠?
이거는 뭐 간단하게 구현할 수 있습니다.
SELECT
CAR_ID,
CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE = '세단' OR 'SUV';
(이 코드에 오류가 있습니다. 뒤에서 다루도록 하겠습니다.)
우리는 'SUV' OR '세단'만 해당 기간(2022-11-01 ~ 2022-11-30)까지 대여 가능한 것만 구해야하므로, 2-1에서 얻은 결과의 CAR_ID를 바탕으로 2번째 테이블에서 'SUV' OR '세단'을 추리는 작업을 해야합니다.
코드는 다음과 같습니다.
SELECT
HISTORY_ID,
CAR_ID,
START_DATE,
END_DATE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE CAR_ID = (SELECT
CAR_ID
-- CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE = '세단' OR 'SUV')
이 코드에는 1가지 문제점이 있습니다.
실행을 해보려고 하니, 이런 문구가 뜨더군요.
"Subquery returns more than 1 row"
이유는 다음과 같습니다.
우리가 서브쿼리를 작성하고, 그 서브쿼리가 결과를 반환할 때, 과연 몇 개의 결과들을 반환할까요?
->1개? OR 여러 개?
바로 여러 개를 반환하겠죠.
'=' 연산자는 단일 값을 비교할 때, 즉 1개만을 반환할 때 쓰는 연산자이고, 우리는 여러 개의 값을 반환하길 원한다면 'IN' 연산자를 사용해야합니다.
<수정 후>
SELECT
HISTORY_ID,
CAR_ID,
START_DATE,
END_DATE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE CAR_ID IN (SELECT
CAR_ID
-- CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE = '세단' OR 'SUV')

위 2-2번 SQL문에서 저 날짜 조건만 추가하면 되겠죠.
SELECT
HISTORY_ID,
CAR_ID,
START_DATE,
END_DATE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE CAR_ID IN (SELECT
CAR_ID
-- CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE = '세단' OR 'SUV') AND (START_DATE >= '2022-11-01' AND END_DATE <= '2022-11-30');
하지만, 또 문제가 생겼습니다.
2-1에서 오류가 생겼다는 그 문제인데요.
'WHERE CAR_TYPE = '세단' OR 'SUV'
코드를 보시면,
*조건 1: CAR_TYPE = '세단' 이지만,
*조건 2: 'SUV'(TRUE)
이렇게 조건 2는 달랑 SUV만 남게 되면서 결과값들이 전부 다 TRUE로 나오게 됩니다.
어떻게 수정하면 좋을까요?
방법은 2가지가 있습니다.

<수정 후>
SELECT
HISTORY_ID,
CAR_ID,
START_DATE,
END_DATE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE CAR_ID IN (SELECT
CAR_ID
-- CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE IN ('세단','SUV') AND (START_DATE >= '2022-11-01' AND END_DATE <= '2022-11-30'));
하지만, 우리는 논리는 맞지만, 코드를 잘못 설계한 부분이 있습니다.
바로 날짜 설정 부분인데요.
문제를 자세히 읽어보면, '2022-11-01' ~ '2022-11-30'일까지 대여 가능해야 합니다.
두번째 테이블에서 START_DATE와 END_DATE는 대여하는 기간을 의미하죠.
그러면, 우리는 반대로 접근해야 합니다.
3가지 방법이 있는데,
- NOT IN
- NOT EXISTS
- 날짜 반대로 적용하기
이렇게 됩니다.
※'NOT IN' VS 'NOT EXISTS'
구분 NOT EXISTSNOT IN기본 개념 “서브쿼리에 해당 조건을 만족하는 행이 하나라도 존재하지 않는다” “서브쿼리 결과 집합에 값이 포함되어 있지 않다” NULL 처리 NULL이 있어도 영향 없음 (존재 유무만 따짐) ✅서브쿼리 결과 중 NULL이 한 개라도 있으면 전체 결과가 FALSE로 취급 ❌ 성능 측면 옵티마이저가 보통 세미조인(anti-semi join)으로 변환해서 빠름 DB 엔진에 따라 NULL 체크 때문에 비효율 가능 가독성 “존재하지 않으면” → 논리적으로 명확 “NOT IN (subquery)” → NULL 포함 시 예외 많음 표에 있는 설명을 좀 더 쉽고 간단하게 하자면,
'NOT EXISTS'는 말 그대로 '존재 유무'에 포커스를 둡니다. 즉, 존재 유무란, 'NULL'(빈 값,없는 값)인지 아니면 'NOT NULL'(있는 값)인지를 비교하는 것이지요.
반면, 'NOT IN'은 그 값이 거기에 포함되어 있는지 OR 아닌지를 판단합니다.
그래서 만약, NOT IN에 NULL값이 포함되어 있으면, NOT IN의 구동 로직은 다음과 같으므로
CAR.CAR_ID != 1 AND CAR.CAR_ID != 2 AND CAR.CAR_ID != NULLNULL을 UNKNOWN 취급하여 FALSE로 만들기 때문에 항상 결과가 FALSE로 나와, 모든 행을 다 안 나오게 끔 필터링됩니다.
반면, NOT EXISTS는 존재 유무(NULL)를 판단하므로 이게 기준이 되어버리는 것이죠.
항목 NOT IN NOT EXISTS NULL 포함 시 결과가 전부 사라질 수 있음 ❌ 영향 없음 ✅ 비교 기준 값 집합 존재 여부 의미 “값이 이 집합 안에 없다” “이 조건 만족하는 행이 없다” 추천 위험 (NULL 주의) 안전 (표준적 방법) 제가 여기서 NULL로 설명하며 유의를 준 이유가 맨 뒷부분에 나오니, 참고하시길 바랍니다.
저는 'NOT IN' 방법으로 진행해보았습니다.
SELECT
HISTORY_ID,
CAR_ID,
START_DATE,
END_DATE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE CAR_ID NOT IN (SELECT
CAR_ID
-- CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE IN ('세단','SUV') AND (START_DATE >= '2022-11-01' AND END_DATE <= '2022-11-30'));
※ 만약, NOT IN이 아니라 NOT EXISTS로 처음부터 코드를 설정했다면, 이렇게 되었을 것입니다.
FROM CAR_RENTAL_COMPANY_CAR c -- 바깥 쿼리 (c). C는 별칭. WHERE NOT EXISTS ( SELECT 1 FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY h -- 안쪽 서브쿼리 (h). H는 별칭. WHERE h.CAR_ID = c.CAR_ID -- 이 부분 주목!(바깥 쿼리의 c를 참조!) AND h.START_DATE <= '2022-11-30' AND h.END_DATE >= '2022-11-01' )여기에 쓰인 개념은 '상관 서브쿼리' 개념입니다.
'상관 서브쿼리'와 '일반 서브쿼리'의 차이점은 다음과 같습니다.
'NOT IN'을 실행한 결과는 다음과 같습니다.

2-3까지 만든 최종 코드로 우리는 첫번째 테이블의 CAR_TYPE을 SELECT할 수 있을까요?
불가능합니다.
그럼 어떻게 CAR_TYPE을 불러올 수 있을까요?
바로 'JOIN'을 사용하는 방법입니다.
어떤 '공통된 열'과 'JOIN'을 하면 될까요?
저는 공통된 열은, PK와 FK인 CAR_ID이고, 내부 조인(교집합)을 하면 될 거 같다는 생각이 들었습니다.
이유는, 어차피 HISTORY 테이블에서 참조하는 CAR_ID FK는 CAR 테이블에 다 있을 것이고,
거기서 교집합을 해도 어차피 HISTORY 테이블은 다 나올 거고, CAR_TYPE도 다 살아있을테니, 여기서 CAR_TYPE만 추가 SELECT 하면 된다고 판단한 것입니다.
SELECT
H.HISTORY_ID,
H.CAR_ID,
C.CAR_TYPE,
H.START_DATE,
H.END_DATE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY H INNER JOIN CAR_RENTAL_COMPANY_CAR C
WHERE C.CAR_ID NOT IN (SELECT
CAR_ID
-- CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE IN ('세단','SUV') AND (START_DATE >= '2022-11-01' AND END_DATE <= '2022-11-30'));
하지만, 결과를 보니 'SUV'와 '세단'외에도 '리무진'과 같은 차 종이 포함되어 있고, 마치 카타시안 곱처럼 결과가 형성된 거처럼 보였습니다.
이와 같은 문제는 제가 NOT IN에서 WHERE절로 조건문을 적을 때, AND로 묶고 이 결과 통째로 NOT IN을 시킨 부분에서 문제가 발생합니다.
따라서, 조건을 분리해야 합니다.
(또한 제가 JOIN시 ON 조건도 빼먹었어요 ㅎㅎ...)
SELECT
H.HISTORY_ID,
H.CAR_ID,
C.CAR_TYPE,
H.START_DATE,
H.END_DATE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY H INNER JOIN CAR_RENTAL_COMPANY_CAR C ON H.CAR_ID = C. CAR_ID
WHERE C.CAR_TYPE IN ('세단','SUV') AND C.CAR_ID NOT IN (SELECT
CAR_ID
-- CAR_TYPE
FROM CAR_RENTAL_COMPANY_CAR
WHERE (H.START_DATE >= '2022-11-01' AND H.END_DATE <= '2022-11-30'));
이러면 IN 조건으로 '세단' OR 'SUV'를 고르고,
NOT IN 조건으로 올바른 날짜 범위를 선택할 수 있습니다.

지금까지 너무 많은 걸 했습니다.
실패도 하고 수정도 하고 등등...
머리도 리프레쉬할 겸, 중간 점검을 해보도록 하겠습니다.
※문제: 3개의 테이블에서 1. 자동차 종류: SUV,세단 2. 2022-11-1 ~ 2022-11-30 대여 가능 3. 대여 금액: 50~200
-> 자동차 ID, 자동차 종류, 대여 금액(FEE) 리스트 출력하라.
-> 대여 금액 기준 DESC, 대여 금액이 같으면 자동차 종류 기준 ASC, 자동차 종류까지 같으면 자동차 ID 기준 DESC
우리는 현재 2단계 거의 끝 마무리 부분에 있는 거 같습니다.
'2단계 다 한 거 아니냐?' 라는 질문에 저의 답은
'아직 수정할 부분이 남아있다.'입니다.
제가 사실 이 날짜 설정 부분에서 수 많은 수정 작업을 거쳤는데, 이걸 다 넣기에는 너무 많고 복잡하니 최대한 간추려보도록 하겠습니다.
※날짜 조건 여기서 제대로 설정 완료하기!!
위 2-4의 최종 결과 사진을 보면,
맨 처음 행의 END_DATE가 2022-11-09임을 알 수 있습니다.하지만, 우리는 온전히 '2022-11-01' ~ '2022-11-30'일까지 FULL로 대여할 수 있게끔 날짜를 설정해야합니다.
그럼 어떻게 하면 될까요?
H.START_DATE <='2022-11-30' AND H.END_DATE >= '2022-11-01'

제가 틀리고 수정한 부분을 다 설명하면 너무 많으므로 바로 최종 답안으로 넘어가서 최종 답안을 설명하도록 하겠습니다.
그럼 앞에 2-1부터 2-5까지 틀린 과정은 왜 설명했냐?에 대해서는, 제가 설명하고 싶은 개념이 있어서 설명했던 점 양해부탁드립니다.
사실상, 이 2-6 코드만 보고 이해하시면 됩니다.
먼저 코드를 보여드리고 핵심만 설명하겠습니다.
<정답 코드> - 이것만 보셔도 됩니다.
SELECT
t.CAR_ID,
t.CAR_TYPE,
t.FEE
FROM (
SELECT
c.CAR_ID,
c.CAR_TYPE,
FLOOR(c.DAILY_FEE * 30 * (100 - dp.DISCOUNT_RATE) / 100) AS FEE
FROM CAR_RENTAL_COMPANY_CAR c
JOIN CAR_RENTAL_COMPANY_DISCOUNT_PLAN dp -- INNER JOIN
ON dp.CAR_TYPE = c.CAR_TYPE
AND dp.DURATION_TYPE = '30일 이상' -- 30일 대여에 해당
WHERE c.CAR_TYPE IN ('세단', 'SUV')
AND NOT EXISTS ( -- 11월과 겹치는 대여가 단 1건도 없어야
SELECT 1
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY h
WHERE h.CAR_ID = c.CAR_ID
AND h.START_DATE <= DATE('2022-11-30')
AND h.END_DATE >= DATE('2022-11-01')
)
) AS t
WHERE t.FEE >= 500000
AND t.FEE < 2000000
ORDER BY t.FEE DESC, t.CAR_TYPE ASC, t.CAR_ID DESC;
※핵심
INNER JOIN 하는 테이블이 다름.
NOT IN이 아닌 NOT EXISTS 사용.
하지만, 여기서는 NOT IN도 쓸 수 있음.
이 3가지에 대해서만 설명하고 글을 마치도록 하겠습니다.
우리는 앞에서 INNER JOIN하는 테이블을 1번째 테이블과 2번째 테이블로 했었죠.
하지만, 여기서는 2번째 테이블과 3번째 테이블을 INNER JOIN 합니다.
그 이유는, 3번째 테이블의 할인율이 달라지면 바로 적용될 수 있게 동기화한 것입니다.
2-6-3에서도 말하겠지만, 여기서는 H라는 2번째 테이블 안에서 서브쿼리를 작성했으므로, NULL과 관련된 부분이 없습니다. 그래서, NOT IN도 사용 가능합니다.
NOT EXISTS로는, 해당 날짜 조건에 부합하는 CAR_ID를 찾는 부분이 되겠네요.
SELECT 1은 아무 의미 없습니다.
우리는 WHERE 조건절에 부합하는 것만 파악하면 되므로, SELECT는 무시해도 됩니다. 다만, 아무 값이나 적은 것입니다.
이에 대한 설명은 2-6-3에서 말씀드렸습니다.
<NOT IN 정답 코드>
SELECT
t.CAR_ID,
t.CAR_TYPE,
t.FEE
FROM (
SELECT
c.CAR_ID,
c.CAR_TYPE,
FLOOR(c.DAILY_FEE * 30 * (100 - dp.DISCOUNT_RATE) / 100) AS FEE
FROM CAR_RENTAL_COMPANY_CAR c
JOIN CAR_RENTAL_COMPANY_DISCOUNT_PLAN dp
ON dp.CAR_TYPE = c.CAR_TYPE
AND dp.DURATION_TYPE = '30일 이상'
WHERE c.CAR_TYPE IN ('세단', 'SUV')
AND c.CAR_ID NOT IN ( -- ← NOT EXISTS와 논리 동일
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'
)
) AS t
WHERE t.FEE >= 500000
AND t.FEE < 2000000
ORDER BY t.FEE DESC, t.CAR_TYPE ASC, t.CAR_ID DESC;
이 코드에서 NOT IN을 보면,
WHERE절에서의 CAR_ID가 같은 조건 대신,
SELECT문에서 CAR_ID를 조회하는 것으로 선택했죠.
이번 게시글은 다른 게시글보다 좀 중구난방인 느낌이 큽니다..
그래서 매우 아쉽습니다..
사실 이 글에서 더 남기고 싶은 말들이 있었는데,
유기적으로 잘 이어지지 않는 거 같아서 제외했습니다.
그 중 하나가 'RIGHT OUTER JOIN' 부분인데,
이 부분은 날짜 조건 설정할 때,
'CAR_RENTAL_COMPANY_CAR' 테이블에는 있지만, 'CAR_RENTAL_COMPANT_RENTAL_HISTORY' 테이블에는 없는,
즉, 차는 있지만 대여를 아예 안 한 상황까지 고려하여 중간에 RIGHT OUTER JOIN으로 변경했었는데,
이 문제에서는 이거까지는 고려는 안 하더라구요.
참 어려운 문제였고..
정리도 잘 안되고...
이 글을 지금 썼지만,
솔직히 불만족스럽습니다.
이 글을 보시는 여러분께서는
여기에 나와있는 개념들과 정답 코드(2개)만 중점적으로 보시면 될 거 같아요.
그럼 이만 마무리하도록 하겠습니다.
긴 글 읽어주셔서 감사합니다!!