학습 목표
DDL과 DML을 구분하고 대표 문법을 읽을 수 있다.
SELECT 절의 논리적 처리 순서를 설명할 수 있다.
INNER JOIN과 LEFT JOIN의 결과 차이를 예로 설명할 수 있다.
GROUP BY·HAVING으로 집계·필터를 할 수 있다.
서브쿼리가 쓰이는 대표 패턴을 인식할 수 있다.
문제 상황
- ORM이 만든 SQL이
SELECT *에JOIN이 여러 개라 느리다- 어떤 절이 먼저 적용되는지 모르면 튜닝이 막막하다
LEFT JOIN인데WHERE orders.id IS NOT NULL을 쓰면 INNER처럼 동작한다GROUP BY없이COUNT(*)와 컬럼을 섞으면 DB가 에러를 낸다- "서브쿼리 vs JOIN" 논쟁만 듣고 언제 무엇을 쓸지 기준이 없다
관계형 모델에서 릴레이션·키·제약을 봤다. SQL은 그 구조를 조작·조회하는 언어다.
1. DDL과 DML
| 분류 | 역할 | 예 |
|---|---|---|
| DDL (Data Definition) | 스키마 정의·변경 | CREATE TABLE, ALTER, DROP |
| DML (Data Manipulation) | 데이터 삽입·변경·삭제·조회 | INSERT, UPDATE, DELETE, SELECT |
| DCL / TCL | 권한, 트랜잭션 | GRANT, COMMIT, ROLLBACK |
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO users (id, email) VALUES (1, 'alice@example.com');
- DDL은 보통 메타데이터 락·마이그레이션 이슈와 연결
- DML은 앱·분석 쿼리의 대부분
2. SELECT 논리 순서
SQL을 쓰는 순서와 DB가 해석하는 순서가 다르다.

SELECT u.email, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at >= '2025-01-01'
GROUP BY u.email
HAVING COUNT(o.id) > 0
ORDER BY order_count DESC
LIMIT 10;
| 순서 | 절 | 역할 |
|---|---|---|
| 1 | FROM / JOIN |
테이블 결합, 카테시안 곱 후 조인 조건 |
| 2 | WHERE |
행 필터 (집계 전) |
| 3 | GROUP BY |
그룹 형성 |
| 4 | HAVING |
그룹 필터 (집계 후) |
| 5 | SELECT |
컬럼·표현식·집계 함수 |
| 6 | ORDER BY |
정렬 |
| 7 | LIMIT |
개수 제한 |
- 옵티마이저는 실행 순서를 바꿀 수 있지만 의미(semantics) 는 위 순서를 따른다
WHEREvsHAVING: 행 조건 vs 그룹 조건
3. JOIN
관계형 모델의 관계를 쿼리로 표현한다.

-- 주문이 있는 사용자만
SELECT u.email, o.id
FROM users u
INNER JOIN orders o ON o.user_id = u.id;
-- 주문 없는 사용자도 포함
SELECT u.email, o.id
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
| JOIN | 결과 |
|---|---|
INNER JOIN |
양쪽에 매칭 있는 행만 |
LEFT JOIN |
왼쪽 전부 + 오른쪽 매칭 (없으면 NULL) |
RIGHT JOIN |
오른쪽 기준 (실무에선 LEFT로 대체 가능) |
FULL OUTER |
양쪽 미매칭까지 (DB별 지원 차이) |
CROSS JOIN |
카테시안 곱 — 의도적이지 않으면 위험 |
ON— 조인 조건,WHERE— 조인 후 필터LEFT JOIN+WHERE o.id IS NOT NULL→ 사실상 INNER
4. GROUP BY와 집계
SELECT status, COUNT(*) AS cnt, AVG(total) AS avg_total
FROM orders
WHERE created_at >= current_date - interval '30 days'
GROUP BY status;
SELECT에 나오는 비집계 컬럼은GROUP BY에 포함되어야 한다 (표준 SQL)HAVING status = 'paid' AND COUNT(*) > 100— 그룹 단위 조건
| 함수 | 용도 |
|---|---|
COUNT(*) |
행 수 |
SUM, AVG |
합·평균 |
MIN, MAX |
최소·최대 |
COUNT(DISTINCT col) |
중복 제외 개수 |
5. 서브쿼리 개요
쿼리 안에 또 쿼리를 넣는 패턴이다.
-- 스칼라 서브쿼리
SELECT email,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS n
FROM users u;
-- IN
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);
-- EXISTS (반복 존재 검사에 유리한 경우 많음)
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
- 상관 서브쿼리 — 안쪽이 바깥 행 참조
- 성능은 옵티마이저·인덱스에 따라 JOIN과 동등 변환되기도, 안 되기도 함
- 복잡하면 CTE(
WITH)로 가독성 확보
WITH active_users AS (
SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id HAVING COUNT(*) >= 5
)
SELECT u.* FROM users u JOIN active_users a ON a.user_id = u.id;
6. 정리
- DDL은 스키마, DML은 데이터 — 앱은 주로 DML
- SELECT는
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY순으로 이해 - JOIN 종류에 따라 누락·NULL 행이 달라진다
- 서브쿼리·JOIN·CTE는 표현 방식; 실행 계획은 별도 확인
다음에 다룰 것
- 정규화
- 함수 종속, 1NF~3NF, 역정규화 트레이드오프
해당 내용은 Database System Concepts, 7/E (Avraham Silberschatz, Henry F. Korth, S. Sudarshan) 의 내용을 기반으로 합니다.
'데이터베이스 따라잡기' 카테고리의 다른 글
| 디스크와 페이지 (0) | 2026.06.23 |
|---|---|
| 쿼리 처리 개요 (0) | 2026.06.22 |
| 정규화 (0) | 2026.06.21 |
| 관계형 모델 (0) | 2026.06.19 |
| 데이터베이스 오리엔테이션 (0) | 2026.06.18 |