Devin.KR

종합 연습 - 자체 모의 문제 30선과 오답 해설

개발자KR 조회 1

이 장에서 배우는 것

이 책의 마지막 장은 새 개념을 배우는 자리가 아니라 지금까지 쌓은 지식을 실전처럼 점검하는 자리다. 모델링 판단부터 윈도 함수까지 다룬 내용을 모두 아우르는 자체 모의고사 30문항을 온라인 서점 스키마 하나로 새로 만들고, 정답만이 아니라 틀리기 쉬운 이유까지 정리한다. 채점은 감으로 하지 않고 SQL로 직접 검증하는 방법도 함께 익힌다.

  • 모델링 8문항, SQL 기본 12문항, SQL 활용 10문항으로 구성된 모의고사를 스스로 풀어 본다
  • 정답과 함께 "왜 그 오답을 고르기 쉬운가"를 설명할 수 있는지 확인한다
  • NOT IN의 NULL 함정, 조인 팬아웃(Fan-out), ROLLUP 소계 처리처럼 반복 출제되는 함정을 유형별로 묶는다
  • 모의고사 채점을 SELECT 문으로 직접 검증하는 습관을 들인다

문제 상황

서점 시스템 운영팀 네 명이 점심시간마다 모여 SQLD 대비 스터디를 한다. 지난주에는 시중 문제집을 30문항씩 풀고 채점표로 정답만 맞춰 봤는데, 정답률은 높았지만 "이 문항은 왜 이 보기가 답인가"를 서로 설명하지 못하는 경우가 많았다. 특히 NOT IN 서브쿼리에 NULL이 섞이는 경우, 두 테이블을 동시에 조인해서 집계할 때 행이 부풀어 오르는 경우, ROLLUP 결과에서 소계 행을 상세 데이터로 착각하는 경우가 반복해서 틀렸다. 이번 주 스터디는 방식을 바꿔서, 시중 문제를 그대로 가져오는 대신 온라인 서점 스키마로 문제를 직접 만들고 정답을 SQL 검증 스크립트로 확인하기로 했다. 이 장은 그 결과물이다.

문제 유형과 자주 나오는 함정

서른 문항을 다시 훑어보면 함정은 몇 가지 패턴으로 좁혀진다. 모델링 문항은 "식별자를 어떻게 잡을지"와 "이력을 남길지"를 착각하는 경우가 많고, SQL 기본 문항은 NULL 비교와 집계 순서(WHERE와 HAVING) 착각이 대부분이다. SQL 활용 문항은 상관 서브쿼리, 윈도 함수 프레임, ROLLUP·GROUPING SETS 결과 해석에서 갈린다. 아래 표로 유형별 대비 요령을 정리한다.

문제 유형별 자주 나오는 함정과 대비 요령
유형자주 나오는 함정대비 요령관련 주제
모델링식별자 설계와 이력 관리를 혼동업무 규칙을 먼저 문장으로 적고 키를 정한다식별·비식별 관계
SQL 기본NULL 비교, WHERE·HAVING 순서 착각실행 순서(FROM→WHERE→GROUP BY→HAVING)를 손으로 적어 본다NULL과 집계 함정
SQL 활용상관 서브쿼리 누락, 프레임 오해서브쿼리가 바깥 행마다 다시 도는지 직접 확인한다서브쿼리·윈도 함수
DML·TCL세션 간 커밋 시점 착각세션을 두 개 열어 커밋 전후를 눈으로 비교한다트랜잭션 제어
DDL·제약CASCADE를 편의로 남발삭제 정책은 업무 요구사항부터 확인한다무결성 제약

이 장의 검증 스크립트에 쓰이는 문법은 MySQL 8과 오라클에서 거의 같지만, 세부 문법은 아래처럼 차이가 있다.

이 장에서 다루는 문법의 MySQL 8과 오라클 차이
항목MySQL 8오라클비고
ROLLUP 문법GROUP BY ROLLUP(a,b)GROUP BY ROLLUP(a,b)둘 다 표준 문법 지원, 결과 동일
소계 행 판별GROUPING(칼럼)GROUPING(칼럼)둘 다 동일 함수 제공
상위 N행LIMIT nFETCH FIRST n ROWS ONLY오라클도 12c부터 FETCH FIRST 지원
자동 채번AUTO_INCREMENTSEQUENCE + TRIGGER 또는 IDENTITY이 장 예제는 값 직접 지정으로 우회

모의고사 30선

모델링 8문항, SQL 기본 12문항, SQL 활용 10문항이다. 문제 원문은 시중 문제집을 참고하지 않고 온라인 서점 도메인으로 새로 썼다.

모델링 8문항

모델링 영역 8문항 정답과 함정
번호문제 요지정답틀리기 쉬운 이유
M1주문상세 기본키 설계(주문번호, 도서번호) 복합키무조건 별도 대리키를 만들어야 한다고 착각
M2회원 등급 변경 이력 반영 여부등급 이력 테이블 분리칼럼 하나로 충분하다고 보고 과거 등급을 버림
M3도서 카테고리를 문자열로 둘지 여부카테고리 테이블 분리목록이 짧다고 트레이드오프 없이 반정규화
M4주문한 회원만 리뷰 작성 가능 규칙 반영주문상세를 참조하도록 식별자 재설계FK만 걸고 규칙 검증을 애플리케이션에만 맡김
M5같은 도서 재주문 허용 여부회원+도서를 유니크로 두지 않음회원과 도서 조합을 유니크로 착각해 재주문을 막음
M6주문 총액 칼럼 반정규화 선행 조건상세 변경 시 총액 갱신 로직 필요조회 성능만 좋아진다고 보고 정합성 유지 방안을 빠뜨림
M7회원 탈퇴 시 주문 이력 처리탈퇴 플래그로 논리 삭제ON DELETE CASCADE로 주문까지 함께 삭제
M8도서 저자가 여러 명인 경우도서저자 연결 테이블 추가저자 이름을 한 칼럼에 콤마로 이어붙임

SQL 기본 12문항

SQL 기본 영역 12문항 정답과 함정
번호문제 요지정답틀리기 쉬운 이유
B1평점이 없는 리뷰 걸러내기rating IS NOT NULLrating <> NULL 로 비교해 항상 결과 없음
B2한 번도 주문 안 된 도서 찾기NOT EXISTS 사용NOT IN 서브쿼리에 NULL이 섞이면 결과가 통째로 사라짐
B3INNER JOIN과 LEFT JOIN 행 수 비교상세 없는 주문 1건만큼 LEFT JOIN이 더 많음주문 건수와 조인 결과 행 수를 같다고 착각
B4집계 칼럼과 비집계 칼럼 동시 조회GROUP BY에 비집계 칼럼 포함설정에 따라 통과되는 걸 표준 정답으로 착각
B5카테고리 평균가 조건 필터링HAVING 절 사용WHERE 절에 집계 조건을 넣어 오류 유발
B6이메일에서 도메인만 추출마지막 '@' 위치 기준 문자열 함수도메인에 점이 여러 개면 위치 계산을 틀림
B7가입 30일 이내 회원 조회날짜 함수로 일수 차이 계산문자열 그대로 비교해 형 변환 오류 위험
B8UNION과 UNION ALL 결과 행 수중복 존재 여부에 따라 달라짐두 집합에 중복이 없다고 가정하고 같다고 답함
B9동명 칼럼이 있는 조인 결과 조회테이블 별칭으로 명시이름이 같아도 자동으로 구분된다고 착각
B10커밋 전 다른 세션의 조회 결과기본 격리수준에서는 보이지 않음같은 세션 기준으로 착각
B11가격 0 이상만 허용하는 제약CHECK(price >= 0)NOT NULL만 걸고 값 범위 제약을 생략
B12회원 삭제 시 주문 처리 옵션RESTRICT로 삭제 차단편의상 CASCADE를 무조건 선택

SQL 활용 10문항

SQL 활용 영역 10문항 정답과 함정
번호문제 요지정답틀리기 쉬운 이유
A1카테고리 평균가보다 비싼 도서상관 서브쿼리 사용서브쿼리가 한 번만 계산된다고 착각(상관관계 누락)
A2서브쿼리 결과에 NULL이 있을 때 IN과 EXISTS 비교EXISTS가 안전두 방식이 항상 같은 결과를 낸다고 착각
A3카테고리별 매출 순위 매기기RANK는 동점이면 다음 등수를 건너뜀RANK와 DENSE_RANK가 같은 결과라고 착각
A4주문일 순 누적 매출 계산기본 프레임은 첫 행부터 현재 행까지매 행마다 전체 합계가 나온다고 착각
A5최근 3건 이동 평균 계산ROWS BETWEEN 2 PRECEDING AND CURRENT ROWROWS와 RANGE 차이를 몰라 동률일 때 결과가 달라짐
A6ROLLUP 소계 행 구분GROUPING(칼럼)=1로 식별소계 행의 NULL을 실제 데이터의 NULL과 혼동
A7카테고리별·등급별 합계만 따로 필요GROUPING SETS((카테고리),(등급))CUBE를 써서 불필요한 조합까지 모두 생성
A8리뷰 답글 계층 구조 조회재귀 CTE로 앵커와 재귀 부분 구분재귀 종료 조건을 빠뜨려 무한 루프 위험
A9회원별 최근 주문 1건만 뽑기ROW_NUMBER로 PARTITION BY 회원GROUP BY와 MAX(날짜)만 써서 다른 칼럼이 어긋남
A10리뷰도 쓰고 주문도 취소한 회원 찾기INTERSECT 또는 이중 EXISTSOR로 묶어 교집합이 아닌 합집합을 구함

완성 코드

아래는 온라인 서점 스키마와 표본 데이터, 그리고 B2·A2·A3·A6 문항을 실제로 검증하는 스크립트다. 파일 하나(mock_review.sql)로 그대로 실행할 수 있다.

-- 온라인 서점 스키마 (모의고사 검증용)
CREATE TABLE member (
    member_id   INT PRIMARY KEY,
    name        VARCHAR(30) NOT NULL,
    grade       VARCHAR(10) NOT NULL,
    join_date   DATE NOT NULL
);

CREATE TABLE book (
    book_id     INT PRIMARY KEY,
    title       VARCHAR(50) NOT NULL,
    category    VARCHAR(20) NOT NULL,
    price       INT NOT NULL CHECK (price >= 0),
    stock_qty   INT NOT NULL CHECK (stock_qty >= 0)
);

CREATE TABLE orders (
    order_id    INT PRIMARY KEY,
    member_id   INT NOT NULL,
    order_date  DATE NOT NULL,
    status      VARCHAR(10) NOT NULL,
    FOREIGN KEY (member_id) REFERENCES member(member_id)
);

CREATE TABLE order_detail (
    order_id    INT NOT NULL,
    book_id     INT NOT NULL,
    quantity    INT NOT NULL CHECK (quantity > 0),
    unit_price  INT NOT NULL,
    PRIMARY KEY (order_id, book_id),
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (book_id) REFERENCES book(book_id)
);

CREATE TABLE review (
    review_id   INT PRIMARY KEY,
    member_id   INT NOT NULL,
    book_id     INT NOT NULL,
    rating      INT,
    review_date DATE NOT NULL
);

-- 표본 데이터
INSERT INTO member VALUES
 (1,'김도윤','VIP','2023-01-10'),
 (2,'이서연','일반','2023-03-22'),
 (3,'박준호','일반','2023-05-02'),
 (4,'최지우','VIP','2024-01-15'),
 (5,'정하늘','일반','2024-02-20');

INSERT INTO book VALUES
 (101,'SQL 실전 가이드','IT전문서',28000,12),
 (102,'여름의 문','소설',15000,3),
 (103,'파이썬 데이터분석','IT전문서',32000,7),
 (104,'느긋한 하루','에세이',14000,20),
 (105,'안개마을','소설',16000,2),
 (106,'자바 기초','IT전문서',26000,0),
 (107,'새 도서 입고 예정','에세이',20000,15);

INSERT INTO orders VALUES
 (1001,1,'2024-03-02','결제완료'),
 (1002,2,'2024-03-05','결제완료'),
 (1003,1,'2024-03-10','결제완료'),
 (1004,3,'2024-03-12','취소'),
 (1005,4,'2024-03-15','결제완료'),
 (1006,2,'2024-03-20','결제완료'),
 (1007,5,'2024-03-25','결제완료'),
 (1008,3,'2024-03-27','결제완료');

INSERT INTO order_detail VALUES
 (1001,101,1,28000),
 (1001,103,1,32000),
 (1002,102,2,15000),
 (1003,104,3,14000),
 (1004,105,1,16000),
 (1005,101,1,28000),
 (1005,106,1,26000),
 (1006,103,1,32000),
 (1006,105,1,16000),
 (1007,101,2,28000);

INSERT INTO review VALUES
 (9001,1,101,5,'2024-03-04'),
 (9002,2,102,4,'2024-03-08'),
 (9003,1,103,5,'2024-03-06'),
 (9004,4,101,3,'2024-03-18'),
 (9005,2,105,4,'2024-03-22');

-- [모의 SQL기본-02] 한 번도 주문되지 않은 도서를 NOT EXISTS로 조회한다
SELECT b.book_id, b.title
FROM book b
WHERE NOT EXISTS (
    SELECT 1 FROM order_detail od WHERE od.book_id = b.book_id
);

-- 답안 자동 검증 예시: 정답이 도서 107 한 건인지 확인한다
SELECT CASE WHEN COUNT(*) = 1 THEN '검증통과' ELSE '검증실패' END AS 판정
FROM book b
WHERE NOT EXISTS (SELECT 1 FROM order_detail od WHERE od.book_id = b.book_id)
  AND b.book_id = 107;

-- [모의 SQL활용-02] 재고 5권 미만 도서를 결제완료 주문으로 산 회원을 EXISTS로 조회한다
SELECT m.member_id, m.name
FROM member m
WHERE EXISTS (
    SELECT 1
    FROM orders o
    JOIN order_detail od ON od.order_id = o.order_id
    JOIN book b ON b.book_id = od.book_id
    WHERE o.member_id = m.member_id
      AND o.status = '결제완료'
      AND b.stock_qty < 5
)
ORDER BY m.member_id;

-- [모의 SQL활용-03] 카테고리별 매출 합계와 순위를 RANK로 구한다
SELECT b.category,
       SUM(od.quantity * od.unit_price) AS total_sales,
       RANK() OVER (ORDER BY SUM(od.quantity * od.unit_price) DESC) AS sales_rank
FROM orders o
JOIN order_detail od ON od.order_id = o.order_id
JOIN book b ON b.book_id = od.book_id
WHERE o.status = '결제완료'
GROUP BY b.category
ORDER BY sales_rank;

-- [모의 SQL활용-06] 카테고리·등급별 매출 소계와 총계를 ROLLUP으로 구한다
SELECT b.category, m.grade,
       SUM(od.quantity * od.unit_price) AS total_sales,
       GROUPING(b.category) AS is_cat_total,
       GROUPING(m.grade) AS is_grade_total
FROM orders o
JOIN member m ON m.member_id = o.member_id
JOIN order_detail od ON od.order_id = o.order_id
JOIN book b ON b.book_id = od.book_id
WHERE o.status = '결제완료'
GROUP BY ROLLUP(b.category, m.grade)
ORDER BY GROUPING(b.category), b.category, GROUPING(m.grade), m.grade;

줄별 해설

  • CREATE TABLE 다섯 개는 회원·도서·주문·주문상세·리뷰 순서로 만들며, order_detail은 (order_id, book_id) 복합키를 기본키로 잡아 M1 문항의 정답과 스키마를 일치시켰다.
  • book에 107번 도서를 넣고 어떤 order_detail 행에도 등장시키지 않아, B2 문항의 "한 번도 주문 안 된 도서"를 재현했다.
  • orders에 1008번 주문을 만들되 order_detail 행을 하나도 연결하지 않았다. 이 주문이 있어야 LEFT JOIN 결과에 NULL이 섞이는 상황(NOT IN 함정)을 실제로 보여줄 수 있다.
  • [모의 SQL기본-02] 쿼리는 NOT EXISTS로 도서마다 order_detail 존재 여부를 다시 확인하므로, 1008번 주문이 있어도 결과가 흔들리지 않는다.
  • 검증 SELECT는 실제 채점 방식을 보여준다. 정답 개수와 조건을 CASE로 비교해 사람이 눈으로 확인하지 않아도 되게 만든다.
  • [모의 SQL활용-02] 쿼리는 EXISTS 안에서 o.member_id = m.member_id로 바깥 행을 다시 참조하는 상관 서브쿼리다. 이 참조를 빼면 항상 같은 결과만 나와 A1·A2 문항이 지적하는 함정에 그대로 걸린다.
  • [모의 SQL활용-03] 쿼리는 RANK를 매출 합계 내림차순으로 매긴다. GROUP BY로 먼저 카테고리별 합계를 만든 뒤 그 결과에 윈도 함수를 적용하는 순서를 눈으로 확인할 수 있다.
  • [모의 SQL활용-06] 쿼리는 GROUP BY ROLLUP(b.category, m.grade)로 상세행, 카테고리 소계, 전체 총계를 한 번에 만든다. GROUPING 함수로 소계·총계 행을 구분해 A6 문항의 정답 근거를 그대로 보여준다.

실행 결과

$ mysql bookstore < mock_review.sql

+---------+--------------------------+
| book_id | title                    |
+---------+--------------------------+
|     107 | 새 도서 입고 예정        |
+---------+--------------------------+
1 row in set

+--------------+
| 판정         |
+--------------+
| 검증통과     |
+--------------+
1 row in set

+-----------+--------+
| member_id | name   |
+-----------+--------+
|         2 | 이서연 |
|         4 | 최지우 |
+-----------+--------+
2 rows in set

+--------------+-------------+------------+
| category     | total_sales | sales_rank |
+--------------+-------------+------------+
| IT전문서     |      202000 |          1 |
| 소설         |       46000 |          2 |
| 에세이       |       42000 |          3 |
+--------------+-------------+------------+
3 rows in set

+--------------+--------+-------------+--------------+----------------+
| category     | grade  | total_sales | is_cat_total | is_grade_total |
+--------------+--------+-------------+--------------+----------------+
| IT전문서     | VIP    |      114000 |            0 |              0 |
| IT전문서     | 일반   |       88000 |            0 |              0 |
| IT전문서     | NULL   |      202000 |            0 |              1 |
| 소설         | 일반   |       46000 |            0 |              0 |
| 소설         | NULL   |       46000 |            0 |              1 |
| 에세이       | VIP    |       42000 |            0 |              0 |
| 에세이       | NULL   |       42000 |            0 |              1 |
| NULL         | NULL   |      290000 |            1 |              1 |
+--------------+--------+-------------+--------------+----------------+
8 rows in set
서브쿼리에 NULL이 섞이면 NOT IN은 결과가 사라지고 NOT EXISTS만 정확한 결과를 낸다

실무에서 자주 틀리는 것

NOT IN에 NULL이 섞인 서브쿼리

아래 틀린 코드는 1008번 주문 때문에 book_id가 NULL인 행이 섞여 있어, NOT IN이 아무 행도 반환하지 않는다.

-- 틀린 코드: 결과가 항상 0행이 된다
SELECT b.book_id, b.title
FROM book b
WHERE b.book_id NOT IN (
    SELECT od.book_id
    FROM orders o
    LEFT JOIN order_detail od ON od.order_id = o.order_id
);

-- 고친 코드: NOT EXISTS는 NULL의 영향을 받지 않는다
SELECT b.book_id, b.title
FROM book b
WHERE NOT EXISTS (
    SELECT 1 FROM order_detail od WHERE od.book_id = b.book_id
);

조인 팬아웃으로 인한 집계 중복

회원 한 명에 주문과 리뷰를 동시에 조인하면 행이 곱해져 COUNT가 실제보다 커진다.

-- 틀린 코드: 회원1은 주문2건 x 리뷰2건 = 4행이 되어 두 건수 모두 4로 나온다
SELECT m.member_id, COUNT(o.order_id) AS 주문건수, COUNT(r.review_id) AS 리뷰건수
FROM member m
JOIN orders o ON o.member_id = m.member_id
JOIN review r ON r.member_id = m.member_id
GROUP BY m.member_id;

-- 고친 코드: DISTINCT로 곱해진 행을 되돌린다
SELECT m.member_id, COUNT(DISTINCT o.order_id) AS 주문건수, COUNT(DISTINCT r.review_id) AS 리뷰건수
FROM member m
JOIN orders o ON o.member_id = m.member_id
JOIN review r ON r.member_id = m.member_id
GROUP BY m.member_id;
두 테이블을 동시에 조인하면 행이 곱해져 COUNT가 부풀어 오른다

ROLLUP 소계 행을 상세 데이터로 착각

카테고리 소계 행은 grade만 NULL이고 category는 값이 남아 있어, category만 걸러서는 소계가 제거되지 않는다.

-- 틀린 코드: 카테고리 소계 행(grade=NULL)이 그대로 남는다
SELECT b.category, m.grade, SUM(od.quantity * od.unit_price) AS total_sales
FROM orders o
JOIN member m ON m.member_id = o.member_id
JOIN order_detail od ON od.order_id = o.order_id
JOIN book b ON b.book_id = od.book_id
GROUP BY ROLLUP(b.category, m.grade)
HAVING b.category IS NOT NULL;

-- 고친 코드: GROUPING으로 소계·총계 행을 명시적으로 제외한다
SELECT b.category, m.grade, SUM(od.quantity * od.unit_price) AS total_sales
FROM orders o
JOIN member m ON m.member_id = o.member_id
JOIN order_detail od ON od.order_id = o.order_id
JOIN book b ON b.book_id = od.book_id
GROUP BY ROLLUP(b.category, m.grade)
HAVING GROUPING(b.category) = 0 AND GROUPING(m.grade) = 0;

필요 없는 조합까지 만드는 CUBE 남용

카테고리별 합계와 등급별 합계만 필요한데 CUBE를 쓰면 카테고리와 등급의 모든 조합까지 함께 나와 해석이 번거로워진다.

-- 틀린 코드: 필요 없는 category x grade 조합까지 모두 나온다
SELECT b.category, m.grade, SUM(od.quantity * od.unit_price) AS total_sales
FROM orders o
JOIN member m ON m.member_id = o.member_id
JOIN order_detail od ON od.order_id = o.order_id
JOIN book b ON b.book_id = od.book_id
GROUP BY CUBE(b.category, m.grade);

-- 고친 코드: 필요한 두 조합만 GROUPING SETS로 지정한다
SELECT b.category, m.grade, SUM(od.quantity * od.unit_price) AS total_sales
FROM orders o
JOIN member m ON m.member_id = o.member_id
JOIN order_detail od ON od.order_id = o.order_id
JOIN book b ON b.book_id = od.book_id
GROUP BY GROUPING SETS ((b.category), (m.grade));
ROLLUP은 상세행 위에 카테고리 소계와 전체 총계를 층층이 쌓아 만든다

한눈에 보기

시험 직전 점검 체크리스트
점검 항목확인할 것
NULL이 들어갈 수 있는 서브쿼리인가NOT IN 대신 NOT EXISTS로 바꿔도 결과가 같은지 확인한다
두 테이블을 동시에 조인해 집계하는가COUNT나 SUM 앞에 DISTINCT가 필요한지 확인한다
ROLLUP·CUBE 결과를 그대로 쓰는가GROUPING 함수로 소계·총계 행을 구분했는지 확인한다
윈도 함수 프레임을 지정했는가ROWS와 RANGE 중 무엇을 의도했는지 명시했는지 확인한다
삭제·갱신 제약을 요구사항과 맞췄는가CASCADE를 습관적으로 쓰지 않았는지 확인한다

연습 문제

  1. member와 orders 테이블로 "결제완료 주문이 한 번도 없는 회원" 목록을 NOT EXISTS로 작성하고, 왜 NOT IN보다 안전한지 서술하라.
  2. order_detail과 book을 조인해 카테고리별 판매 도서 종수를 구하는 쿼리를 작성하고, COUNT(*) 대신 무엇을 써야 하는지 설명하라.
  3. GROUPING SETS를 이용해 회원 등급별 합계와 전체 합계만 구하고 카테고리별 합계는 제외하는 쿼리를 작성하라.
  4. 재귀 CTE와 CONNECT BY 중 MySQL 8에서 쓸 수 있는 것은 무엇이며 그 이유는 무엇인가.

정답과 해설

  1. SELECT m.member_id, m.name
    FROM member m
    WHERE NOT EXISTS (
        SELECT 1 FROM orders o
        WHERE o.member_id = m.member_id AND o.status = '결제완료'
    );
    

    NOT IN은 서브쿼리 결과에 NULL이 하나라도 있으면 전체 조건이 거짓이 되어 결과가 사라진다. NOT EXISTS는 행 단위로 존재 여부만 확인하므로 NULL의 영향을 받지 않는다. 본문 표본 데이터에서는 모든 회원이 결제완료 주문을 한 건 이상 가지고 있어 결과가 0행이며, 이 역시 정상적인 정답이다.

  2. SELECT b.category, COUNT(DISTINCT od.book_id) AS 판매도서종수
    FROM order_detail od
    JOIN book b ON b.book_id = od.book_id
    GROUP BY b.category;
    

    COUNT(*)는 주문상세 행 수를 세므로 같은 도서를 여러 주문에서 팔았을 때 중복으로 잡힌다. COUNT(DISTINCT book_id)로 세어야 실제 판매된 도서 종수가 나온다. 표본 데이터로는 IT전문서 3종, 소설 2종, 에세이 1종이다.

  3. SELECT m.grade, SUM(od.quantity * od.unit_price) AS total_sales
    FROM orders o
    JOIN member m ON m.member_id = o.member_id
    JOIN order_detail od ON od.order_id = o.order_id
    WHERE o.status = '결제완료'
    GROUP BY GROUPING SETS ((m.grade), ());
    

    GROUPING SETS에 카테고리를 넣지 않고 (등급)과 빈 집합 ()만 지정하면 등급별 소계와 전체 총계만 나온다. 표본 데이터로는 VIP 156,000원, 일반 134,000원, 총계 290,000원이 나와 앞서 구한 전체 매출과 일치한다.

  4. MySQL 8은 WITH RECURSIVE로 재귀 공통테이블식(Recursive CTE)을 지원하지만 CONNECT BY 구문은 지원하지 않는다. CONNECT BY는 오라클 고유의 계층 질의 확장이고, 표준 SQL과 MySQL은 재귀 CTE로 같은 기능을 표현한다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.