문제
공유 자전거 서비스인 따릉이를 운영하는 팀에서는 지역별로 이용률이 감소하고 있는 따릉이 정류소를 폐쇄해 따릉이 서비스의 운영비용을 줄이려고 합니다. 따릉이 데이터를 다루는 당신은 정류소 정보가 담겨있는 station 테이블과 대여 기록이 담겨 있는 rental_history 테이블을 확인해 폐쇄를 검토할 따릉이 정류소 목록을 추려내는 업무를 받았습니다.
폐쇄할 정류소를 검토하기 위해 2019년 10월 한 달 동안 정류소에서 발생한 대여/반납 건수가 2018년 10월 같은 정류소에서 발생한 대여/반납 건수의 50% 이하인 정류소를 출력하는 쿼리를 작성해주세요. 2018년 10월 또는 2019년 10월 한 달간 대여/반납 건수가 0건인 정류소는 이미 폐쇄된 정류소이거나 새로이 생긴 정류소 일 수 있으므로 쿼리 결과에 포함되지 않도록 해주세요. 쿼리 결과에는 아래 컬럼이 포함되어 있어야 합니다.
결론부터 말하면 여태 푼 solvesql 문제 중에서는 가장 어려웠다. 우선 나의 초기 쿼리
WITH
TEMP1 AS (
SELECT
rent_station_id,
COUNT() AS cnt
FROM
rental_history
WHERE
strftime ("%Y-%m", rent_at) = "2018-10"
GROUP BY
rent_station_id
),
TEMP2 AS (
SELECT
return_station_id,
COUNT() AS cnt2
FROM
rental_history
WHERE
strftime ("%Y-%m", return_at) = "2018-10"
GROUP BY
return_station_id
),
TEMP3 AS (
SELECT
rent_station_id,
COUNT() AS cnt3
FROM
rental_history
WHERE
strftime ("%Y-%m", rent_at) = "2019-10"
GROUP BY
rent_station_id
),
TEMP4 AS (
SELECT
return_station_id,
COUNT() AS cnt4
FROM
rental_history
WHERE
strftime ("%Y-%m", return_at) = "2019-10"
GROUP BY
return_station_id
)
SELECT
TEMP1.rent_station_id AS station_id,
station.name,
station.local,
ROUND ( ( (cnt3 + cnt4) * 1.0 / (cnt + cnt2) * 100.0 ), 2 ) AS usage_pct
FROM
TEMP2
JOIN TEMP1 ON TEMP1.rent_station_id = TEMP2.return_station_id
JOIN TEMP3 ON TEMP3.rent_station_id = TEMP1.rent_station_id
JOIN TEMP4 ON TEMP4.return_station_id = TEMP1.rent_station_id
JOIN station ON station.station_id = TEMP2.return_station_id
WHERE (cnt3 + cnt4) * 1.0 / (cnt + cnt2) * 100.0 <= 50 AND (cnt3 + cnt4 != 0 AND cnt + cnt2 != 0)
이 쿼리는 오답이다. 2018년에 대여는 했지만 반납은 하지않은 특수한 경우를 고려하지 않았기 때문이다. 이걸 해결하려면?
COALESCE 와 FULL OUTER JOIN을 사용해야 한다. 우선 정답 쿼리부터 보자
WITH
TEMP1 AS (
SELECT
rent_station_id,
COUNT(*) AS cnt
FROM
rental_history
WHERE
strftime ("%Y-%m", rent_at) = "2018-10"
GROUP BY
rent_station_id
),
TEMP2 AS (
SELECT
return_station_id,
COUNT(*) AS cnt2
FROM
rental_history
WHERE
strftime ("%Y-%m", return_at) = "2018-10"
GROUP BY
return_station_id
),
TEMP3 AS (
SELECT
rent_station_id,
COUNT(*) AS cnt3
FROM
rental_history
WHERE
strftime ("%Y-%m", rent_at) = "2019-10"
GROUP BY
rent_station_id
),
TEMP4 AS (
SELECT
return_station_id,
COUNT(*) AS cnt4
FROM
rental_history
WHERE
strftime ("%Y-%m", return_at) = "2019-10"
GROUP BY
return_station_id
)
SELECT
COALESCE(TEMP1.rent_station_id, TEMP2.return_station_id) AS station_id,
station.name,
station.local,
ROUND ( ( (COALESCE(cnt3, 0) + COALESCE(cnt4, 0)) * 1.0 / (COALESCE(cnt, 0) + COALESCE(cnt2, 0)) * 100.0 ), 2 ) AS usage_pct
FROM
TEMP1
FULL OUTER JOIN TEMP2 ON TEMP1.rent_station_id = TEMP2.return_station_id
LEFT JOIN TEMP3 ON COALESCE(TEMP1.rent_station_id, TEMP2.return_station_id) = TEMP3.rent_station_id
LEFT JOIN TEMP4 ON COALESCE(TEMP1.rent_station_id, TEMP2.return_station_id) = TEMP4.return_station_id
JOIN station ON station.station_id = COALESCE(TEMP1.rent_station_id, TEMP2.return_station_id)
WHERE (COALESCE(cnt3, 0) + COALESCE(cnt4, 0)) * 1.0 / (COALESCE(cnt, 0) + COALESCE(cnt2, 0)) * 100.0 <= 50
AND (COALESCE(cnt3, 0) + COALESCE(cnt4, 0) > 0 AND COALESCE(cnt, 0) + COALESCE(cnt2, 0) > 0)
COALESCE
COALESCE는 SQL에서 NULL 값을 처리하는 함수다. 여러 개 값중에서 첫 번째로 NULL이 아닌 값을 반환한다.
모든 값이 NULL이면 NULL을 반환한다. 이 쿼리에서는 왜 사용할까? LEFT 쿼리를 썼을 떄 겹치지 않는 부분이 있으면 NULL을 반환하는데 이것이 문제다. 만약 정류소 A에 2018년 대여는 있지만 반잡이 없으면 TEMP2에 정류소 A가 없어서 cnt2가 NULL이 된다. cnt + cnt2는 10 + NULL = NULL 이 되어서 계산 오류가 난다. 그러나 COALESCE(cnt, 0) + COALESCE(cnt2, 0)로 하면 10 + 0 = 10으로 정상 계산이 된다.
그리고 아래 부분이 해깔릴건데 이부분은 클로드 도움을 받았다. 시각화 설명이다.
🔍 SQL 쿼리 실행 여정
cnt3 = NULL
cnt4 = NULL
COALESCE(NULL, 0) = 0
COALESCE(NULL, 0) = 0
0 + 0 = 0
(정상 계산)
2019년 이용량 ÷ 2018년 이용량 × 100 ≤ 50
2018년 이용량 > 0 AND 2019년 이용량 > 0
🎉 최종 결과
모든 조건을 만족하는 정류소들이 station 테이블과 JOIN되어
정류소명, 지역정보와 함께 최종 결과를 출력합니다!
폐쇄 검토 대상: 4개 정류소
2. COALESCE의 역할: NULL 값으로 인한 계산 오류를 방지하고 의미 있는 결과를 만듭니다.
3. WHERE 절의 이중 검증: 비즈니스 요구사항(50% 이하)과 데이터 품질(0이 아닌 값) 모두를 확인합니다.
4. 단계적 접근: 복잡한 쿼리도 작은 단계들로 나누어 이해하면 명확해집니다.
문제는 이 쿼리는 쓰레기다. WITH을 4번이나 쓴건 심각한 성능 문제가 있다. 어떻게 최적화 해야할지 몰랐다. 이 부분도 클로드 도움을 받았다.
WITH station_usage AS (
SELECT
COALESCE(rent_station_id, return_station_id) AS station_id,
SUM(
CASE
WHEN strftime("%Y-%m", COALESCE(rent_at, return_at)) = "2018-10" THEN 1
ELSE 0
END
) AS usage_2018,
SUM(
CASE
WHEN strftime("%Y-%m", COALESCE(rent_at, return_at)) = "2019-10" THEN 1
ELSE 0
END
) AS usage_2019
FROM (
SELECT rent_station_id, NULL as return_station_id, rent_at, NULL as return_at
FROM rental_history
WHERE strftime("%Y-%m", rent_at) IN ("2018-10", "2019-10")
UNION ALL
SELECT NULL as rent_station_id, return_station_id, NULL as rent_at, return_at
FROM rental_history
WHERE strftime("%Y-%m", return_at) IN ("2018-10", "2019-10")
) combined_data
GROUP BY COALESCE(rent_station_id, return_station_id)
HAVING usage_2018 > 0 AND usage_2019 > 0
)
SELECT
su.station_id,
s.name,
s.local,
ROUND((su.usage_2019 * 100.0 / su.usage_2018), 2) AS usage_pct
FROM station_usage su
JOIN station s ON s.station_id = su.station_id
WHERE (su.usage_2019 * 100.0 / su.usage_2018) <= 50.0
ORDER BY usage_pct ASC;
'MYSQL > solvesql' 카테고리의 다른 글
전국 카페 주소 데이터 정제하기 (1) | 2025.06.21 |
---|---|
멀티 플랫폼 게임 찾기 (0) | 2025.06.21 |
멘토링 짝꿍 리스트 (0) | 2025.06.16 |
지역별 주문의 특징 (0) | 2025.06.06 |
백분율 계산 할때는 *100.0 하기 (0) | 2025.06.02 |