[프로그래머스] ⭐⭐⭐⭐ 그룹별 조건에 맞는 식당 목록 출력하기 (MySQL)
📌 문제 설명
다음은 고객의 정보를 담은 MEMBER_PROFILE 테이블과 식당의 리뷰 정보를 담은 REST_REVIEW 테이블입니다.
MEMBER_PROFILE 테이블
| Column name | Type | Nullable | Description |
|---|---|---|---|
| MEMBER_ID | VARCHAR(100) | FALSE | 회원 ID |
| MEMBER_NAME | VARCHAR(50) | FALSE | 회원 이름 |
| TLNO | VARCHAR(50) | TRUE | 회원 연락처 |
| GENDER | VARCHAR(1) | TRUE | 성별 |
| DATE_OF_BIRTH | DATE | TRUE | 생년월일 |
REST_REVIEW 테이블
| Column name | Type | Nullable | Description |
|---|---|---|---|
| REVIEW_ID | VARCHAR(10) | FALSE | 리뷰 ID |
| REST_ID | VARCHAR(10) | TRUE | 식당 ID |
| MEMBER_ID | VARCHAR(100) | TRUE | 회원 ID |
| REVIEW_SCORE | NUMBER | TRUE | 점수 |
| REVIEW_TEXT | VARCHAR(1000) | TRUE | 리뷰 텍스트 |
| REVIEW_DATE | DATE | TRUE | 리뷰 작성일 |
📌 문제
MEMBER_PROFILE와 REST_REVIEW 테이블에서 리뷰를 가장 많이 작성한 회원의 리뷰들을 조회하는 SQL문을 작성해주세요. 회원 이름, 리뷰 텍스트, 리뷰 작성일이 출력되도록 작성해주시고, 결과는 리뷰 작성일을 기준으로 오름차순, 리뷰 작성일이 같다면 리뷰 텍스트를 기준으로 오름차순 정렬해주세요.
주의사항: REVIEW_DATE의 날짜 포맷이 예시와 동일해야 정답 처리됩니다.
📌 예시
MEMBER_PROFILE 테이블이 다음과 같고
| MEMBER_ID | MEMBER_NAME | GENDER | DATE_OF_BIRTH |
|---|---|---|---|
| jiho92@naver.com | 이지호 | W | 1992-02-12 |
| yelin1130@gmail.com | 조예린 | W | 1990-11-30 |
| seoyeons@naver.com | 박서연 | W | 1993-03-16 |
REST_REVIEW 테이블이 다음과 같을 때
| REVIEW_ID | MEMBER_ID | REVIEW_SCORE | REVIEW_TEXT | REVIEW_DATE |
|---|---|---|---|---|
| R000000066 | yelin1130@gmail.com | 5 | 김치찌개 최곱니다. | 2022-02-12 |
| R000000067 | yelin1130@gmail.com | 5 | 햄이 많아서 좋아요 | 2022-02-22 |
| R000000065 | soobin97@naver.com | 5 | 부찌 국물에서 샤브샤브 맛이나고 깔끔 | 2022-04-12 |
| R000000068 | ksyi0316@gmail.com | 5 | 숙성회가 끝내줍니다. | 2022-02-15 |
| R000000069 | yoonsy95@naver.com | 4 | 비린내가 전혀없어요. | 2022-04-16 |
SQL을 실행하면 다음과 같이 출력되어야 합니다.
| MEMBER_NAME | REVIEW_TEXT | REVIEW_DATE |
|---|---|---|
| 조예린 | 김치찌개 최곱니다. | 2022-02-12 |
| 조예린 | 햄이 많아서 좋아요 | 2022-02-22 |
🧩 문제 구조 파악
이 문제는 2단계 접근이 핵심입니다. 단계를 분리하지 않고 한 쿼리에서 해결하려 하면 아래에서 설명할 함정에 빠집니다.
🚨 내가 틀렸던 과정 (디버깅 일지)
❌ 시도 1 — GROUP BY + LIMIT 1로 한 번에 해결하려 함
SELECT m.MEMBER_NAME, r.REVIEW_TEXT, r.REVIEW_DATE
FROM MEMBER_PROFILE AS m
JOIN REST_REVIEW AS r ON m.MEMBER_ID = r.MEMBER_ID
GROUP BY m.MEMBER_ID
ORDER BY count(m.MEMBER_ID) DESC, r.REVIEW_DATE, r.REVIEW_TEXT
LIMIT 1
핵심 규칙: “가장 많이 쓴 회원 찾기”와 “그 회원의 모든 리뷰 가져오기”는 반드시 두 단계로 분리해야 합니다.
💻 코드
ver(1) — WHERE 절 서브쿼리 활용 ✅ (내 최종 풀이)
핵심 아이디어: WHERE 절의 서브쿼리로 리뷰가 가장 많은 회원의 ID를 먼저 추출하고, 메인 쿼리에서 해당 회원의 모든 리뷰를 출력합니다.
SELECT
m.MEMBER_NAME,
r.REVIEW_TEXT,
DATE_FORMAT(r.REVIEW_DATE, '%Y-%m-%d') AS REVIEW_DATE -- 날짜 포맷을 YYYY-MM-DD로 맞춤
FROM MEMBER_PROFILE AS m
JOIN REST_REVIEW AS r ON m.MEMBER_ID = r.MEMBER_ID -- 회원 ID 기준으로 두 테이블 연결
WHERE m.MEMBER_ID = (
-- 서브쿼리: 리뷰 수가 가장 많은 회원의 ID 1개를 추출
-- GROUP BY로 회원별 리뷰 수를 집계 → 내림차순 정렬 → 1등만 반환
SELECT MEMBER_ID
FROM REST_REVIEW
GROUP BY MEMBER_ID
ORDER BY COUNT(*) DESC
LIMIT 1
)
ORDER BY r.REVIEW_DATE ASC, r.REVIEW_TEXT ASC;
팁. ORDER BY r.REVIEW_DATE ASC, r.REVIEW_TEXT ASC 대신 ORDER BY 3, 2처럼 SELECT 절의 컬럼 순서 번호를 사용할 수 있습니다. 컬럼명이 길 때 쿼리를 간결하게 줄여주는 실무 팁입니다.
ver(2) — CTE + RANK() 윈도우 함수 활용 💡 (공동 1등 완벽 대응)
핵심 아이디어: LIMIT 1은 공동 1등이 있을 때 1명만 뽑는 문제가 있습니다. RANK() 윈도우 함수로 순위를 매기고 WHERE rnk = 1로 공동 1등을 모두 포함시킵니다.
-- CTE: 회원별 리뷰 수를 집계하고 리뷰 수 기준 순위를 매김
WITH ReviewRanks AS (
SELECT
MEMBER_ID,
-- RANK(): 리뷰 수 내림차순으로 순위 부여
-- 공동 1등이 있으면 모두 rnk = 1 → ROW_NUMBER()와의 핵심 차이
RANK() OVER (ORDER BY COUNT(*) DESC) AS rnk
FROM REST_REVIEW
GROUP BY MEMBER_ID
)
SELECT
m.MEMBER_NAME,
r.REVIEW_TEXT,
DATE_FORMAT(r.REVIEW_DATE, '%Y-%m-%d') AS REVIEW_DATE
FROM MEMBER_PROFILE m
JOIN REST_REVIEW r ON m.MEMBER_ID = r.MEMBER_ID
WHERE m.MEMBER_ID IN (
-- 1위인 회원 ID를 모두 가져옴 (공동 1등도 전부 포함)
SELECT MEMBER_ID FROM ReviewRanks WHERE rnk = 1
)
ORDER BY r.REVIEW_DATE ASC, r.REVIEW_TEXT ASC;
주의. LIMIT 1 vs RANK() = 1 — LIMIT 1은 공동 1등이 있어도 1명만 선택합니다. 데이터 무결성을 보장하려면 RANK() = 1이 더 안전합니다.
ver(3) — CTE + MAX()로 공동 1등 처리 (가독성 중시)
핵심 아이디어: CTE로 회원별 리뷰 수를 먼저 계산하고, MAX()로 최대 리뷰 수를 구한 뒤 동일한 수를 가진 회원을 모두 필터링합니다. 중첩 서브쿼리 없이 읽기 쉬운 구조입니다.
-- Step 1. CTE: 회원별 총 리뷰 수 계산
WITH ReviewCount AS (
SELECT
MEMBER_ID,
COUNT(*) AS review_cnt
FROM REST_REVIEW
GROUP BY MEMBER_ID
)
SELECT
m.MEMBER_NAME,
r.REVIEW_TEXT,
DATE_FORMAT(r.REVIEW_DATE, '%Y-%m-%d') AS REVIEW_DATE
FROM MEMBER_PROFILE m
JOIN REST_REVIEW r ON m.MEMBER_ID = r.MEMBER_ID
WHERE m.MEMBER_ID IN (
-- Step 2. 최대 리뷰 수와 동일한 회원만 선택 → 공동 1등 전부 포함
SELECT MEMBER_ID
FROM ReviewCount
WHERE review_cnt = (SELECT MAX(review_cnt) FROM ReviewCount)
)
ORDER BY r.REVIEW_DATE ASC, r.REVIEW_TEXT ASC;
ver(4) — 인라인 뷰 + HAVING MAX() (구버전 MySQL 대응)
핵심 아이디어: WITH 절(MySQL 8.0+)을 쓸 수 없는 구버전 환경에서 동일한 로직을 중첩 서브쿼리로 구현합니다.
SELECT
m.MEMBER_NAME,
r.REVIEW_TEXT,
DATE_FORMAT(r.REVIEW_DATE, '%Y-%m-%d') AS REVIEW_DATE
FROM MEMBER_PROFILE m
JOIN REST_REVIEW r ON m.MEMBER_ID = r.MEMBER_ID
WHERE m.MEMBER_ID IN (
SELECT MEMBER_ID
FROM REST_REVIEW
GROUP BY MEMBER_ID
-- HAVING으로 최대 리뷰 수와 동일한 회원만 필터링
HAVING COUNT(*) = (
SELECT MAX(review_cnt)
FROM (
SELECT COUNT(*) AS review_cnt -- 가장 안쪽: 회원별 리뷰 수 집계
FROM REST_REVIEW
GROUP BY MEMBER_ID
) AS cnt_table -- MAX()를 적용하기 위한 임시 테이블
)
)
ORDER BY r.REVIEW_DATE ASC, r.REVIEW_TEXT ASC;
팁. HAVING COUNT(*) = MAX(COUNT(*)) 처럼 집계 함수를 중첩해 쓰는 것은 MySQL에서 허용되지 않습니다. 반드시 서브쿼리로 한 번 더 감싸서 MAX()가 적용될 테이블을 먼저 만들어줘야 합니다.
📊 풀이법 비교
📝 이번 문제에서 배운 것
핵심 교훈: “가장 많은 N 찾기”와 “그 N의 전체 데이터 가져오기”는 반드시 두 단계로 분리해야 합니다.
-
GROUP BY는 행을 압축한다. GROUP BY를 쓰는 순간 여러 리뷰가 1행으로 줄어들어 전체 데이터를 잃습니다. 집계가 목적이 아니라면 서브쿼리로 분리하세요.
-
LIMIT 1의 위치가 결과를 완전히 바꾼다. 메인 쿼리 끝의 LIMIT 1은 “전체 결과 1줄”이지만, 서브쿼리 안의 LIMIT 1은 “1등 ID 추출”입니다. 위치가 곧 의미입니다.
-
DATE_FORMAT은 필수 습관으로. DATE 타입 컬럼은 항상
DATE_FORMAT(컬럼, '%Y-%m-%d')로 포맷을 명시하세요. 채점 서버마다 기본 출력 형식이 다를 수 있습니다. -
공동 1등을 고려하면 RANK() 또는 MAX()가 LIMIT 1보다 안전하다. 코딩 테스트를 넘어 실무를 바라본다면 데이터 무결성을 보장하는 쿼리를 습관화하세요.
댓글남기기