5 분 소요

프로그래머스 사이트 링크


📌 문제 설명

다음은 고객의 정보를 담은 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_PROFILEREST_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단계 접근이 핵심입니다. 단계를 분리하지 않고 한 쿼리에서 해결하려 하면 아래에서 설명할 함정에 빠집니다.

graph TD A[REST_REVIEW 전체] --> B[GROUP BY MEMBER_ID\nCOUNT 집계] B --> C[ORDER BY COUNT DESC\nLIMIT 1] C --> D["최다 리뷰 회원 ID\n ex. yelin1130@gmail.com"] D --> E[WHERE MEMBER_ID = 서브쿼리 결과] F[MEMBER_PROFILE] --> G[JOIN REST_REVIEW\non MEMBER_ID] G --> E E --> H[해당 회원의 모든 리뷰 출력\nORDER BY DATE, TEXT]

🚨 내가 틀렸던 과정 (디버깅 일지)

❌ 시도 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
⚠ 함정 1 — GROUP BY 압축
GROUP BY로 회원을 묶으면 리뷰 N개가 1행으로 압축됩니다. REVIEW_TEXT는 DB가 임의로 1개만 반환하므로 전체 리뷰를 볼 수 없습니다.
⚠ 함정 2 — LIMIT 1 오해
LIMIT 1은 "리뷰가 가장 많은 회원 1명"이 아니라 "전체 결과에서 딱 1줄만" 출력하라는 의미입니다. 해당 회원의 리뷰가 10개여도 1줄만 나옵니다.
⚠ 함정 3 — DATE_FORMAT 누락
REVIEW_DATE를 그대로 출력하면 2022-02-12 00:00:00처럼 시간까지 붙어 나옵니다. 채점 서버는 포맷을 엄격히 검사하므로 DATE_FORMAT 처리가 필수입니다.

핵심 규칙: “가장 많이 쓴 회원 찾기”와 “그 회원의 모든 리뷰 가져오기”는 반드시 두 단계로 분리해야 합니다.


💻 코드

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()가 적용될 테이블을 먼저 만들어줘야 합니다.


📊 풀이법 비교

graph LR A[공동 1등 없음이 확실] --> B["ver(1)\nWHERE 서브쿼리\n가장 짧고 빠름"] C[공동 1등 대비 필요] --> D["ver(2) CTE + RANK()\n순위 기반, 직관적"] C --> E["ver(3) CTE + MAX()\n가독성 최우선"] C --> F["ver(4) 인라인 뷰\nMySQL 구버전 대응"]
✅ 코딩 테스트 추천
ver(1) — 코드가 가장 짧아 빠르게 작성할 수 있습니다. 테스트 데이터에 공동 1등이 없다면 이것으로 충분합니다.
💡 실무 추천
ver(3) — CTE로 단계가 명확히 분리되고, MAX()로 공동 1등도 안전하게 처리합니다. 유지보수하기 가장 좋은 구조입니다.

📝 이번 문제에서 배운 것

핵심 교훈: “가장 많은 N 찾기”와 “그 N의 전체 데이터 가져오기”는 반드시 두 단계로 분리해야 합니다.

  1. GROUP BY는 행을 압축한다. GROUP BY를 쓰는 순간 여러 리뷰가 1행으로 줄어들어 전체 데이터를 잃습니다. 집계가 목적이 아니라면 서브쿼리로 분리하세요.

  2. LIMIT 1의 위치가 결과를 완전히 바꾼다. 메인 쿼리 끝의 LIMIT 1은 “전체 결과 1줄”이지만, 서브쿼리 안의 LIMIT 1은 “1등 ID 추출”입니다. 위치가 곧 의미입니다.

  3. DATE_FORMAT은 필수 습관으로. DATE 타입 컬럼은 항상 DATE_FORMAT(컬럼, '%Y-%m-%d')로 포맷을 명시하세요. 채점 서버마다 기본 출력 형식이 다를 수 있습니다.

  4. 공동 1등을 고려하면 RANK() 또는 MAX()가 LIMIT 1보다 안전하다. 코딩 테스트를 넘어 실무를 바라본다면 데이터 무결성을 보장하는 쿼리를 습관화하세요.

댓글남기기