[SECTION10. 통계성 쿼리 실습]
관리자 화면에 흔히 나오는 “오늘의 통계” 같은 형태입니다.
SELECT
(SELECT COUNT(*) FROM member) AS total_member,
(SELECT COUNT(*) FROM board) AS total_board,
(SELECT COUNT(*) FROM comment) AS total_comment,
(SELECT COUNT(*) FROM board_like) AS total_like;
서브쿼리를 SELECT 안에 여러 개 넣으면, 서로 다른 테이블의 집계를 한 줄의 결과로 모을 수 있습니다.
-- 카테고리별 게시글 수, 평균 조회수, 최고 조회수
SELECT
category_id,
COUNT(*) AS board_count,
AVG(view_count) AS avg_view,
MAX(view_count) AS max_view
FROM board
GROUP BY category_id
ORDER BY board_count DESC;
-- 게시글이 5개 이상이면서 평균 조회수가 0보다 큰 카테고리만
SELECT
category_id,
COUNT(*) AS board_count,
AVG(view_count) AS avg_view
FROM board
GROUP BY category_id
HAVING COUNT(*) >= 5 AND AVG(view_count) > 0;
“게시글 작성 수 + 댓글 작성 수”를 기준으로 활동량이 많은 회원을 가려내고 싶을 때, 우선 각각 따로 집계해봅니다.
-- 회원별 게시글 수
SELECT member_id, COUNT(*) AS board_count
FROM board
GROUP BY member_id;
-- 회원별 댓글 수
SELECT member_id, COUNT(*) AS comment_count
FROM comments
GROUP BY member_id;
두 결과를 합쳐서 보는 방법은 다음 단계(JOIN)에서 본격적으로 다룹니다. 지금은 각각 따로 집계하는 연습에 집중합니다.
게시글을 “인기글(조회수 10 이상)”과 “일반글”로 나누어 각각 몇 개인지 한 번에 보고 싶다면 CASE WHEN을 사용합니다.
SELECT
SUM(CASE WHEN view_count >= 10 THEN 1 ELSE 0 END) AS popular_count,
SUM(CASE WHEN view_count < 10 THEN 1 ELSE 0 END) AS normal_count
FROM board;
동작 원리: CASE WHEN은 행 하나하나를 조건에 맞게 1 또는 0으로 바꿔주고, SUM이 그 값들을 더해서 개수처럼 사용하는 방식입니다.
-- 카테고리별로 인기글/일반글 개수를 나누어 집계
SELECT
category_id,
SUM(CASE WHEN view_count >= 10 THEN 1 ELSE 0 END) AS popular_count,
SUM(CASE WHEN view_count < 10 THEN 1 ELSE 0 END) AS normal_count
FROM board
GROUP BY category_id;
-- 좋아요를 가장 많이 받은 게시글 TOP 5 (board_id와 좋아요 수만)
SELECT board_id, COUNT(*) AS like_count
FROM board_like
GROUP BY board_id
ORDER BY like_count DESC
LIMIT 5;
-- 좋아요를 가장 많이 누른 회원(활동적인 회원) TOP 5
SELECT member_id, COUNT(*) AS like_given_count
FROM board_like
GROUP BY member_id
ORDER BY like_given_count DESC
LIMIT 5;
H2에서는 날짜 함수로 날짜 단위를 추출할 수 있습니다.
-- 날짜별 게시글 작성 수 (시간은 빼고 날짜만 추출)
SELECT CAST(created_at AS DATE) AS write_date, COUNT(*) AS board_count
FROM board
GROUP BY CAST(created_at AS DATE)
ORDER BY write_date DESC;
SELECT DATE_FORMAT(created_at, '%Y-%m') AS write_month, COUNT(*) AS board_count
FROM board
GROUP BY write_month
ORDER BY write_month DESC;
날짜 함수는 DBMS마다 문법이 조금씩 다릅니다 (
FORMATDATETIME은 H2 문법). 추후 MySQL로 옮길 때는DATE_FORMAT(created_at, '%Y-%m')처럼 바뀐다는 점을 알아두면 좋습니다.
1. member, board, comment, board_like 테이블의 전체 건수를 한 줄의 결과로 조회하는 SQL을 작성하시오. (서브쿼리 사용)
SELECT
(SELECT COUNT(*) FROM member) AS total_member,
(SELECT COUNT(*) FROM board) AS total_board,
(SELECT COUNT(*) FROM comments) AS total_comment,
(SELECT COUNT(*) FROM board_like) AS total_like;
2. board 테이블에서 카테고리별 게시글 수와 평균 조회수를 함께 조회하고, 게시글 수가 많은 순으로 정렬하는 SQL을 작성하시오.
select category_id,
count(*) as board_count,
avg(view_count) as avg_view
from board
group by category_id
order by board_count desc
3. board 테이블에서 조회수가 10 이상인 게시글 수와, 10 미만인 게시글 수를 CASE WHEN을 사용해서 한 줄로 조회하는 SQL을 작성하시오.
select
sum(case when view_count >= 10 then 1 else 0 END) as popular_count,
sum(case when view_count < 10 then 1 else 0 END) as normal_count
from board
4. board 테이블에서 카테고리별로 인기글(조회수 10 이상)과 일반글(조회수 10 미만) 개수를 각각 구하는 SQL을 작성하시오. (GROUP BY + CASE WHEN 사용)
select category_id,
sum(case when view_count >= 10 then 1 else 0 END) as popular_count,
sum(case when view_count < 10 then 1 else 0 END) as normal_count
from board
group by category_id
5. board_like 테이블에서 좋아요를 가장 많이 누른(활동적인) 회원 TOP 3을 조회하는 SQL을 작성하시오.
select member_id, count(*) from board_like
group by member_id
order by count(*) desc
limit 3
[SECTION11. JOIN 개념 — INNER JOIN, LEFT JOIN]
게시글 목록을 보여줄 때, 보통 작성자의 “이름”이 함께 보여야 합니다. 그런데 board 테이블에는 이름이 없고 member_id만 있습니다.
SELECT * FROM board;
| id | title | member_id |
|---|---|---|
| 1 | 첫 게시글 | 1 |
| 2 | 두번째 게시글 | 2 |
여기서 “1번 회원이 누구지?”를 알려면 member 테이블을 따로 조회해야 합니다.
SELECT * FROM member WHERE id = 1;
매번 이렇게 두 번 조회하는 건 비효율적입니다. JOIN은 두 테이블을 연결해서, 한 번의 쿼리로 양쪽 테이블의 컬럼을 동시에 가져오는 방법입니다.
select
board.title,
member.member_id,
member.username
from board
join member on board.member_id = member.member_id
| title | name |
|---|---|
| 첫 게시글 | 홍길동 |
| 두번째 게시글 | 김철수 |
이제 게시글 제목과 작성자 이름이 한 줄로 합쳐졌습니다.
JOIN이라고만 쓰면 기본적으로 INNER JOIN을 의미합니다. 두 테이블에서 연결 조건이 일치하는 행만 결과로 가져옵니다.
SELECT 컬럼들
FROM 테이블A
INNER JOIN 테이블B ON 테이블A.연결컬럼 = 테이블B.연결컬럼;
SELECT b.id, b.title, m.name AS writer_name
FROM board b
INNER JOIN member m ON b.member_id = m.id;
테이블명에
b,m처럼 짧은 별칭(alias)을 붙이면 컬럼을 적을 때마다 긴 테이블명을 반복하지 않아도 됩니다.FROM board b는 “board 테이블을 b라고 부르겠다”는 뜻입니다.
member 테이블 board 테이블
id=1 홍길동 member_id=1 (게시글A)
id=2 김철수 member_id=1 (게시글B)
id=3 이영희 member_id=2 (게시글C)
member_id=99 (게시글D) ← 99번 회원은 없음!
INNER JOIN 결과:
| 게시글 | 작성자 |
|---|---|
| 게시글A | 홍길동 |
| 게시글B | 홍길동 |
| 게시글C | 김철수 |
게시글D는 결과에서 사라집니다. member_id=99인 회원이 member 테이블에 없기 때문입니다. INNER JOIN은 양쪽 테이블에 모두 존재하는, 즉 “교집합”에 해당하는 데이터만 보여줍니다.
실무에서는 보통 FK 제약조건이 걸려있어서 이런 상황(존재하지 않는 member_id)이 잘 생기지 않습니다. 하지만 “왜 일부 데이터가 안 보이지?”라는 버그를 디버깅할 때 INNER JOIN의 이 특성을 꼭 기억해야 합니다.
select
b.title, m.username, c.category_name
from board as b
inner join
member as m on b.member_id = m.member_id
inner join
category as c on c.category_id = b.category_id
테이블을 3개까지 연결했습니다. JOIN은 필요한 만큼 계속 이어붙일 수 있습니다.
LEFT JOIN은 FROM 뒤에 먼저 적은(왼쪽) 테이블의 데이터는 모두 유지하고, 오른쪽 테이블에 연결되는 데이터가 없으면 NULL로 채웁니다.
SELECT 컬럼들
FROM 테이블A
LEFT JOIN 테이블B ON 테이블A.연결컬럼 = 테이블B.연결컬럼;
게시글 목록을 보여줄 때, “댓글이 0개인 게시글도 목록에는 나와야 합니다.” 만약 INNER JOIN을 쓰면 어떻게 될까요?
-- ❌ INNER JOIN을 쓰면 댓글이 하나도 없는 게시글은 결과에서 사라짐
select * from board b
inner join
comments c on b.board_id = c.board_id
select b.title, c.content
from board b
left join comments c on b.board_id = c.board_id
결과 예시:
| title | content |
|---|---|
| 첫 게시글 | 환영합니다! |
| 첫 게시글 | 반갑습니다 |
| 두번째 게시글 | NULL |
“두번째 게시글”은 댓글이 없지만, LEFT JOIN 덕분에 게시글 자체는 결과에 남아있고 content만 NULL로 표시됩니다.
| INNER JOIN | LEFT JOIN | |
|---|---|---|
| 기준 | 양쪽 테이블에 모두 존재하는 데이터만 | 왼쪽 테이블은 무조건 전부 포함 |
| 연결 안 되는 데이터 | 결과에서 제외됨 | 오른쪽 컬럼이 NULL로 표시됨 |
| 사용 시점 | “둘 다 있는 경우만 보고 싶을 때” | “왼쪽 기준으로 빠짐없이 보고 싶을 때” |
실무 판단 기준: “댓글이 없는 게시글도 목록에 보여야 하나?” → 그렇다면 LEFT JOIN. “댓글이 있는 게시글만 보고 싶다” → INNER JOIN.
-- 게시글별 댓글 수 (댓글이 0개인 게시글도 0으로 표시되어야 함)
select
b.board_id,
b.title,
count(c.comment_id)
from board as b
left join comments c on b.board_id = c.board_id
group by b.board_id, b.title
COUNT(c.id)처럼 컬럼명을 넣으면 NULL인 행은 세지 않기 때문에, 댓글이 없는 게시글은 자동으로 0으로 계산됩니다. 만약COUNT(*)를 쓰면 NULL이어도 행 자체는 존재하므로 잘못된 결과(1로 표시됨)가 나올 수 있어 주의해야 합니다.
select
b.board_id as 글번호,
b.title as 제목,
b.view_count as 조회수,
m.username as 작성자,
c.category_name as 카테고리명,
count(cm.comment_id) as 댓글수
from board as b
inner join member as m on b.member_id = m.member_id
inner join category as c on c.category_id = b.category_id
left join comments as cm on b.board_id = cm.board_id
group by b.board_id, b.title, b.view_count, m.username, c.category_name
order by b.board_id desc
해석:
member)와 카테고리(category)는 게시글에 항상 존재해야 하므로 INNER JOINcomment)은 없을 수도 있으므로 LEFT JOIN이 한 줄의 쿼리가 바로 실제 게시판 목록 화면에서 쓰이는 형태의 쿼리입니다.
1. board와 member를 INNER JOIN해서, 게시글 제목과 작성자 이름을 함께 조회하는 SQL을 작성하시오.
select
board.board_id as 글번호,
board.title as 글제목,
member.username as 작성자
from board
inner join member
on board.member_id = member.member_id
2. board와 category를 INNER JOIN해서, 게시글 제목과 카테고리 이름을 함께 조회하는 SQL을 작성하시오.
select
board.title,
category.category_name
from board
inner join category
on board.category_id = category.category_id
3. board와 comments를 LEFT JOIN해서, 댓글이 없는 게시글도 포함하여 게시글 제목과 댓글 내용을 조회하는 SQL을 작성하시오.
select
board.title as 게시글제목,
comments.content as 댓글내용
from board
left join comments
on board.board_id = comments.board_id
4. board와 comments를 LEFT JOIN하여, 게시글별 댓글 수를 구하는 SQL을 작성하시오. (댓글이 0개인 게시글도 0으로 표시되어야 한다.)
select
board.board_id as 글번호,
board.title as 게시글제목,
count(comments.comment_id)
from board
left join comments
on board.board_id = comments.board_id
group by board.board_id
5. board, member, category 세 테이블을 모두 INNER JOIN해서, 게시글 제목, 작성자 이름, 카테고리 이름을 함께 조회하는 SQL을 작성하시오.
select
b.title,
m.username,
c.category_name
from board as b
inner join member as m on b.member_id = m.member_id
inner join category as c on b.category_id = c.category_id
6. member와 board_like를 LEFT JOIN하여, 좋아요를 한 번도 누르지 않은 회원도 포함해서 회원별 좋아요 누른 횟수를 조회하는 SQL을 작성하시오.
select
m.member_id,
count(m.member_id)
from member as m
left join board_like as b
on m.member_id = b.member_id
group by m.member_id
7. board, member, comment를 활용해서, 게시글 제목, 작성자 이름, 댓글 수를 함께 조회하는 SQL을 작성하시오. (작성자는 항상 존재하므로 INNER JOIN, 댓글은 없을 수 있으므로 LEFT JOIN 사용)
select
b.title as 게시글제목,
m.username as 작성자,
count(c.comment_id) as 댓글수
from board as b
inner join member as m on b.member_id = m.member_id
inner join comments as c on b.board_id = c.comment_id
group by b.title, m.username
[SECTION12. JOIN 실전 실습 — 게시글-회원, 게시글-댓글]
게시판 메인에서 보이는 목록입니다. 제목, 작성자, 카테고리, 조회수, 작성일이 필요합니다.
SELECT
b.id,
b.title,
m.name AS writer_name,
c.name AS category_name,
b.view_count,
b.created_at
FROM board b
INNER JOIN member m ON b.member_id = m.id
INNER JOIN category c ON b.category_id = c.id
ORDER BY b.created_at DESC;
-- 자유게시판(category_id=1)만 보여주기
SELECT
b.id,
b.title,
m.name AS writer_name,
b.view_count,
b.created_at
FROM board b
INNER JOIN member m ON b.member_id = m.id
WHERE b.category_id = 1
ORDER BY b.created_at DESC;
이제 member.name으로 직접 검색할 수 있습니다. (SECTION08에서는 id를 미리 찾아야 했던 작업입니다.)
-- 작성자 이름이 '홍길동'인 게시글 검색
SELECT b.title, m.name AS writer_name
FROM board b
INNER JOIN member m ON b.member_id = m.id
WHERE m.name = '홍길동';
특정 게시글 하나를 클릭했을 때 보이는 화면입니다. 게시글 정보 + 작성자 정보가 필요합니다.
SELECT
b.id,
b.title,
b.content,
b.view_count,
m.name AS writer_name,
m.email AS writer_email,
c.name AS category_name,
b.created_at
FROM board b
INNER JOIN member m ON b.member_id = m.id
INNER JOIN category c ON b.category_id = c.id
WHERE b.id = 1;
게시글 상세를 열 때는 조회수도 함께 증가시키는 것이 보통입니다. (SECTION05에서 배운 UPDATE 활용)
UPDATE board SET view_count = view_count + 1 WHERE id = 1;
게시글 상세 화면에는 보통 그 글에 달린 댓글 목록도 함께 보여줍니다. 댓글에는 “누가 썼는지”도 같이 나와야 합니다.
SELECT
cm.id,
cm.content,
m.name AS commenter_name,
cm.created_at
FROM comment cm
INNER JOIN member m ON cm.member_id = m.id
WHERE cm.board_id = 1
ORDER BY cm.created_at ASC;
댓글은 보통 오래된 순(ASC)으로 보여주는 것이 일반적입니다. 게시글 목록은 최신순(DESC), 댓글은 오래된순(ASC)이라는 차이를 기억해두면 좋습니다.
목록 화면에서 “댓글 3개”처럼 댓글 수를 같이 보여주고 싶을 때입니다.
SELECT
b.id,
b.title,
m.name AS writer_name,
b.view_count,
COUNT(cm.id) AS comment_count
FROM board b
INNER JOIN member m ON b.member_id = m.id
LEFT JOIN comment cm ON b.id = cm.board_id
GROUP BY b.id, b.title, m.name, b.view_count
ORDER BY b.created_at DESC;
작성자는 INNER JOIN(항상 존재), 댓글은 LEFT JOIN(없을 수 있음)을 사용한 이유를
SECTION11에서 배운 내용과 연결해서 다시 떠올려봅니다.
SELECT
b.id,
b.title,
m.name AS writer_name,
COUNT(DISTINCT cm.id) AS comment_count,
COUNT(DISTINCT bl.member_id) AS like_count
FROM board b
INNER JOIN member m ON b.member_id = m.id
LEFT JOIN comment cm ON b.id = cm.board_id
LEFT JOIN board_like bl ON b.id = bl.board_id
GROUP BY b.id, b.title, m.name
ORDER BY b.created_at DESC;
댓글과 좋아요를 동시에 LEFT JOIN하면, 한 게시글에 댓글 3개 + 좋아요 2개가 있을 경우 행이 3 × 2 = 6개로 부풀어 오릅니다(곱집합 현상) 이 상태에서
COUNT(cm.id)를 그냥 쓰면 잘못된 수가 나옵니다. 이런 경우COUNT(DISTINCT 컬럼)을 사용해서 중복을 제거하고 세야 정확한 값이 나옵니다.
SELECT
b.title,
COUNT(cm.id) AS comment_count
FROM board b
LEFT JOIN comment cm ON b.id = cm.board_id
WHERE b.member_id = 1
GROUP BY b.id, b.title;
COUNT(DISTINCT 컬럼)으로 정확하게 집계해야 한다.1. board와 member를 JOIN해서, 작성자 이름이 '김철수'인 게시글의 제목과 작성일을 조회하는 SQL을 작성하시오.
SELECT
board.title,
CAST(board.created_at AS DATE) AS created_date
FROM board
INNER JOIN MEMBER ON board.member_id = member.member_id
WHERE member.username = '김철수';
2. board, member, category를 모두 JOIN해서, 카테고리가 '공지사항'인 게시글의 제목과 작성자 이름을 작성일 최신순으로 조회하는 SQL을 작성하시오.
select
b.board_id,
b.title,
m.username,
b.created_at
from board as b
inner join member as m on b.member_id = m.member_id
inner join category as c on b.category_id = c.category_id
where c.category_name = '공지사항'
order by b.board_id desc
3. id가 5인 게시글에 달린 모든 댓글을, 댓글 작성자 이름과 함께 오래된 순서로 조회하는 SQL을 작성하시오.
select
m.username as 작성자이름,
c.content
from comments as c
inner join member as m on c.member_id = m.member_id
where board_id = 5
order by board_id desc;
4. 게시글 목록을 제목, 작성자 이름, 댓글 수와 함께 조회하는 SQL을 작성하시오. 댓글이 없는 게시글도 댓글 수 0으로 포함되어야 한다.
select
board.title as 글제목,
member.username as 작성자,
count(comments.comment_id) as 댓글수
from board
inner join member on board.member_id = member.member_id
left join comments on board.board_id = comments.board_id
group by board.title, member.username