개발자혜수 홈으로 이동개발자혜수

Hyesu's Blog

테크블로그

공부하면서 기록을 남겨요

데이터베이스: SQL로 데이터 다루기 - SELECT부터 서브쿼리까지

2026.08.15. DB

지난 글에서 테이블을 어떻게 나누고 관계를 맺을지(정규화)를 정리했다면, 이번엔 그렇게 설계한 테이블을 실제로 조회하고 조작하는 SQL을 정리해볼게요.

예제는 이전 글에서 썼던 student, course, enrollment 스키마를 그대로 이어서 쓸게요.

student

student_id name grade
1001 김철수 2
1002 이영희 3
1003 박민수 1
1004 최지훈 2

course

course_id course_name professor
C01 알고리즘 박교수
C02 DB개론 최교수
C03 자료구조 박교수

enrollment

student_id course_id
1001 C01
1001 C02
1002 C01
1004 C03

박민수(1003)는 아직 아무 강좌도 신청하지 않은 상태로 남겨뒀어요. JOIN 파트에서 이 학생이 왜 중요한 예시가 되는지 보여드릴게요.

SELECT, WHERE — 조회와 필터링


SELECT — 조회할 컬럼 지정

SELECT student_id, name FROM student;
SELECT * FROM student; -- 전체 컬럼 (성능·가독성 때문에 실무에선 지양)

WHERE — 행 필터링

조건에 맞는 행만 걸러내요. (뒤에서 볼 GROUP BY보다 먼저 적용돼요.)

SELECT * FROM student WHERE grade >= 2;
SELECT * FROM student WHERE name LIKE '김%';        -- 패턴 매칭
SELECT * FROM student WHERE grade IN (2, 3);        -- 여러 값 중 하나
SELECT * FROM student WHERE grade BETWEEN 1 AND 3;  -- 범위

WHERE grade >= 2의 결과는 이렇게 나와요.

student_id name grade
1001 김철수 2
1002 이영희 3
1004 최지훈 2

JOIN — 여러 테이블 결합


FK로 연결된 여러 테이블을 합쳐서 조회할 때 사용해요. 종류별로 결과가 어떻게 달라지는지 표로 먼저 볼게요.

종류 결과
INNER JOIN 양쪽 테이블에 모두 매칭되는 행만
LEFT JOIN 왼쪽 테이블은 전부 + 오른쪽은 있으면 값, 없으면 NULL
RIGHT JOIN 오른쪽 테이블은 전부 + 왼쪽은 있으면 값, 없으면 NULL
FULL OUTER JOIN 양쪽 테이블 전부, 매칭 안 되는 쪽은 NULL

MySQL은 FULL OUTER JOIN 문법을 직접 지원하지 않아서, LEFT JOINRIGHT JOIN 결과를 UNION으로 합쳐서 흉내 내는 경우가 많아요.

🤔 왜 MySQL 은 FULL OUTER JOIN 을 직접 지원하지 않나요?

🤔 PostgreSQL 과 MySQL 의 차이점은 또 무엇이 있나요? 그리고 어떨 때 사용해야 하나요?

INNER JOIN 예시

SELECT s.name, e.course_id
FROM student s
INNER JOIN enrollment e ON s.student_id = e.student_id;
name course_id
김철수 C01
김철수 C02
이영희 C01
최지훈 C03

수강 기록이 없는 박민수는 결과에서 아예 빠져요. 이게 INNER JOIN의 특징이에요.

LEFT JOIN 예시

SELECT s.name, e.course_id
FROM student s
LEFT JOIN enrollment e ON s.student_id = e.student_id;
name course_id
김철수 C01
김철수 C02
이영희 C01
박민수 NULL
최지훈 C03

이번엔 박민수도 남지만, 매칭되는 enrollment 행이 없어서 course_idNULL로 채워져요.

ON과 WHERE, 조건을 어디에 넣느냐의 차이

INNER JOIN과 “JOIN 후 WHERE로 조건 거는 것”은 결과가 같을 때가 많지만, LEFT JOIN에서는 조건을 ON에 넣느냐 WHERE에 넣느냐에 따라 결과가 완전히 달라져요.

조건을 ON에 넣으면 — 왼쪽 행은 그대로 다 유지돼요.

SELECT s.name, e.course_id
FROM student s
LEFT JOIN enrollment e
  ON s.student_id = e.student_id AND e.course_id = 'C01';
name course_id
김철수 C01
이영희 C01
박민수 NULL
최지훈 NULL

같은 조건을 WHERE로 옮기면 — NULL인 행까지 걸러지면서 사실상 INNER JOIN처럼 동작해요.

SELECT s.name, e.course_id
FROM student s
LEFT JOIN enrollment e ON s.student_id = e.student_id
WHERE e.course_id = 'C01';
name course_id
김철수 C01
이영희 C01

박민수, 최지훈이 사라진 이유는 e.course_idNULL이라 WHERE e.course_id = 'C01' 조건을 통과하지 못했기 때문이에요. “왼쪽 테이블은 무조건 다 보고 싶다”면 조건을 꼭 ON에 넣어야 해요.

GROUP BY, HAVING — 그룹별 집계


같은 값을 가진 행들을 하나의 그룹으로 묶어서, 그룹 단위로 집계 함수를 적용해요.

SELECT grade, COUNT(*) AS student_count
FROM student
GROUP BY grade;
grade student_count
1 1
2 2
3 1

GROUP BY 이후에 조건을 걸고 싶으면 WHERE가 아니라 HAVING을 써요. (WHERE는 그룹화 전 개별 행 필터, HAVING은 그룹화 후 집계 결과 필터예요.)

SELECT grade, COUNT(*) AS student_count
FROM student
GROUP BY grade
HAVING COUNT(*) >= 2;
grade student_count
2 2

인원이 1명뿐인 1학년, 3학년 그룹은 조건을 만족하지 못해 결과에서 빠졌어요.

실행 순서 (작성 순서와 다름 — 면접에서 자주 물어봐요)

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT

WHERE가 SELECT보다 먼저 실행되기 때문에, SELECT에서 만든 별칭(alias)을 WHERE 조건에 쓸 수 없어요. (MySQL은 예외적으로 GROUP BY, HAVING, ORDER BY에서는 별칭 사용을 허용해요.)

INSERT, UPDATE, DELETE — 데이터 변경


INSERT — 새 행 추가

INSERT INTO student (student_id, name, grade)
VALUES (1005, '한소희', 3);

-- 여러 행 한 번에
INSERT INTO student (student_id, name, grade)
VALUES (1006, '정우성', 1), (1007, '송지효', 2);

UPDATE — 기존 행 수정

UPDATE student
SET grade = 2
WHERE student_id = 1003;

⚠️ WHERE 절을 빼먹으면 테이블 전체 행이 수정돼요. 실무 사고의 단골 원인이라, 항상 먼저 SELECT ... WHERE ...로 대상 행을 확인한 뒤 UPDATE를 실행하는 습관이 안전해요.

DELETE — 행 삭제

DELETE FROM student WHERE student_id = 1003;
마찬가지로 WHERE 없으면 전체 삭제예요.

참고 — DELETE vs TRUNCATE vs DROP

명령 대상 롤백 가능 여부 속도
DELETE 행(row) 트랜잭션 안에서 가능 느림 (행 단위로 로그를 남김)
TRUNCATE 테이블 데이터 전체 DB/설정에 따라 다름 (대부분 제한적) 빠름 (테이블을 통째로 초기화)
DROP 테이블 자체(구조 + 데이터) 불가 가장 빠름

집계 함수(Aggregate Functions)


여러 행의 값을 하나의 값으로 요약해요. 보통 GROUP BY와 함께 써요.

함수 설명
COUNT() 행 개수
SUM() 합계
AVG() 평균
MAX() / MIN() 최댓값 / 최솟값
SELECT AVG(grade) FROM student;                     -- 전체 평균
SELECT grade, COUNT(*) FROM student GROUP BY grade;  -- 학년별 인원수
주의: COUNT(*)는 NULL을 포함한 모든 행을 세지만, COUNT(컬럼명)은 해당 컬럼이 NULL이 아닌 행만 세요.

서브쿼리(Subquery)


쿼리 안에 들어있는 또 다른 쿼리예요. 위치에 따라 역할이 달라져요.

1) WHERE절 서브쿼리 — 조건에 사용

SELECT name FROM student
WHERE student_id IN (
  SELECT student_id FROM enrollment WHERE course_id = 'C01'
);

→ “C01 강좌를 듣는 학생 이름 조회” → 결과: 김철수, 이영희

2) FROM절 서브쿼리 (인라인 뷰) — 임시 테이블처럼 사용

SELECT grade_summary.grade, grade_summary.cnt
FROM (
  SELECT grade, COUNT(*) AS cnt FROM student GROUP BY grade
) AS grade_summary
WHERE grade_summary.cnt > 1;

→ 앞서 GROUP BY로 만든 학년별 인원수 결과를 다시 테이블처럼 감싸서, 2명 이상인 학년만 걸러요. 결과: grade = 2, cnt = 2

3) SELECT절 서브쿼리 (스칼라 서브쿼리) — 값 하나를 컬럼처럼 사용

SELECT name,
  (SELECT COUNT(*) FROM enrollment e WHERE e.student_id = s.student_id) AS course_count
FROM student s;
name course_count
김철수 2
이영희 1
박민수 0
최지훈 1

이렇게 바깥 쿼리의 값(s.student_id)을 안쪽 쿼리가 참조하는 걸 상관 서브쿼리(Correlated Subquery)라고 해요. 바깥 행마다 서브쿼리가 다시 실행되기 때문에 일반 서브쿼리보다 느릴 수 있어요 — 실무에서는 JOIN으로 바꿔 최적화하는 경우가 많아요. 위 예제도 JOIN + GROUP BY로 바꾸면 이렇게 쓸 수 있어요.

SELECT s.name, COUNT(e.course_id) AS course_count
FROM student s
LEFT JOIN enrollment e ON s.student_id = e.student_id
GROUP BY s.student_id, s.name;

서브쿼리 vs JOIN은 같은 결과를 낼 수 있는 경우가 많지만, 실행계획과 성능이 달라질 수 있어서 실무·면접에서 자주 비교되는 주제예요.

🤔 그렇다면 같은 결과이지만 성능에 차이를 줄 수 있는 쿼리문 예시는?

정리하며


SELECT/WHERE로 원하는 행을 골라내고, JOIN으로 여러 테이블을 합치고, GROUP BY/HAVING으로 그룹 단위 집계를 뽑아내는 흐름을 정리했어요. 여기에 INSERT/UPDATE/DELETE로 데이터를 바꾸고, 서브쿼리로 쿼리 안에 쿼리를 넣어 더 복잡한 조건을 표현할 수 있다는 것까지 봤어요.

특히 LEFT JOIN에서 조건을 ON에 넣을지 WHERE에 넣을지, 서브쿼리를 JOIN으로 바꿀지 말지는 결과와 성능에 직접 영향을 주니 실무에서 자주 마주치는 포인트예요.
목록으로 돌아가기

댓글