C
Database/SQL_기초/Lesson 02

SQL 기초 — RDBMS·SELECT·WHERE·GROUP BY·JOIN

60분·theory
이 챕터
1/2

SQL 기초 — RDBMS·SELECT·WHERE·GROUP BY·JOIN

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ SELECT · WHERE · GROUP BY · HAVING · ORDER BY 절 정확한 순서
  • ✅ INNER JOIN vs LEFT JOIN 차이 + 케이스 선택
  • ✅ EXPLAIN 으로 실행계획 읽는 법 (type · rows 컬럼)

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

RDBMS 핵심 6가지

한 줄: 관계형 DB = 테이블 + PK/FK + 정규화 + ACID + 인덱스 + SQL. 50년 표준.

6가지 핵심 개념

개념의미한 줄 예시
테이블·행·열데이터의 2D 격자CREATE TABLE users (id, name, email)
PK · FK고유 식별 + 관계orders.user_id REFERENCES users(id)
정규화 1·2·3NF중복 제거 → 이상현상 방지1NF(원자값) → 2NF(부분종속) → 3NF(이행종속)
ACID트랜잭션 4 보장Atomic · Consistent · Isolated · Durable
인덱스B+Tree로 O(log n) 조회조회 ↑, 쓰기 약간 ↓
SQL 4부분언어 분류DDL·DML·DCL·TCL

정규화 단계 빠른 이해

단계규칙위반 예시 → 해결
1NF원자값만 (배열·반복그룹 X)hobbies='읽기,코딩' → 별도 테이블
2NF1NF + PK 전체에 종속(주문ID, 상품ID) PK 에 상품명 종속 → 상품 테이블 분리
3NF2NF + 비키 컬럼 간 종속 Xuser_id → city → 우편번호 → 우편번호 테이블 분리
BCNF모든 결정자가 후보키거의 동일, 더 엄격

> 💡 실무: 보통 3NF 까지. 성능 위해 의도적 비정규화 (조회 잦은 곳).

SQL 4부분

분류용도명령
DDL (Definition)스키마CREATE·ALTER·DROP·TRUNCATE
DML (Manipulation)데이터SELECT·INSERT·UPDATE·DELETE
DCL (Control)권한GRANT·REVOKE
TCL (Transaction)트랜잭션BEGIN·COMMIT·ROLLBACK·SAVEPOINT

SELECT 7절 실행 순서

작성 순서 vs 실행 순서

code
작성: SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
실행: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
       ──────────────────────────────────────────────────────
       데이터를 *준비* 한 후에 컬럼을 *선택* 하고 *정렬* + *자르기*

각 절 — 역할과 흔한 함정

역할흔한 함정해결
FROM · JOIN테이블 지정큰 테이블 먼저 → 메모리 ↑작은 테이블·필터 먼저
WHERE행 필터WHERE UPPER(col)='X' → 인덱스 XWHERE col='X'
WHERE행 필터WHERE col LIKE '%abc' → 인덱스 XLIKE 'abc%'
GROUP BY그룹화SELECT 에 그룹키·집계함수 외 컬럼 → 에러모두 그룹키 또는 집계
HAVING그룹 필터WHERE 와 혼동HAVING SUM>1000 (집계 후)
SELECT컬럼 선택SELECT * → 불필요 데이터 전송필요 컬럼만
ORDER BY정렬인덱스 없으면 전체 정렬 (O(n log n))정렬 컬럼 인덱스
LIMIT · OFFSET페이징OFFSET 100000 → 매우 느림keyset (WHERE id > last_id)

빠른 암기 팁

  • FWG·HSO·L — From-Where-Group / Having-Select-Order / Limit
  • 데이터 줄이기 우선 — WHERE 가 빠를수록 뒤 단계 가벼움
  • 인덱스 안 타는 패턴 5가지: 함수·LIKE 좌측%·형변환·NOT·OR

JOIN + GROUP BY 패턴

JOIN 5종 비교

종류의미사용
INNER양쪽 매칭만주문 + 결제 (둘 다 있는 것)
LEFT왼쪽 모두 + 매칭 (없으면 NULL)모든 사용자 + 주문 (안 한 사람도)
RIGHTLEFT 의 반대거의 안 씀 (LEFT 로 뒤집기)
FULL양쪽 모두 (UNION 효과)양쪽 unique 조회
CROSS카테시안 곱 (M × N)보통 실수, 모든 조합

JOIN 안티 패턴

안티 조인 (한쪽에만 있는 것):

sql
SELECT u.* FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;          -- 주문 한 번도 안 한 사용자

자기 조인 (셀프 참조):

sql
SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

GROUP BY 6 패턴

#패턴예시
1단순 그룹GROUP BY user_id + COUNT(*)
2다중 컬럼GROUP BY user_id, DATE(created_at)
3HAVING (그룹 필터)HAVING COUNT(*) > 5
4시간 단위GROUP BY DATE_TRUNC('day', created_at)
5CASE WHEN 집계SUM(CASE WHEN status='paid' THEN 1 ELSE 0 END)
6ROLLUP · CUBE자동 소계·총계 (GROUP BY ROLLUP(year, month))

WHERE vs HAVING

sql
SELECT user_id, COUNT(*) AS cnt
FROM orders
WHERE status = 'PAID'        -- 행 필터 (실행 1단계)
GROUP BY user_id             -- 그룹화
HAVING COUNT(*) > 5;         -- 그룹 필터 (집계 후)

💾 실전 1 — 사용자별 주문 통계 (LEFT JOIN + GROUP BY)

아래 SQL 실행 결과를 직접 확인하세요. ▶ 실행으로 결과 테이블이 펼쳐집니다.
📥 입력 DB 서버로 전송되는 쿼리
SELECT FROM: users length: 413 chars
-- 입력: users 테이블 + orders 테이블
-- 처리: LEFT JOIN 으로 주문 안 한 사용자도 포함 + GROUP BY 로 사용자별 집계
-- 출력: 사용자별 주문 수, 총 결제액 (주문 0건은 0 표시)

SELECT
  u.id,
  u.name,
  COUNT(o.id) AS order_count,           -- 주문 개수
  COALESCE(SUM(o.amount), 0) AS total   -- 총 결제액 (NULL → 0)
FROM users u
LEFT JOIN orders o
  ON o.user_id = u.id
  AND o.status = 'PAID'                  -- 결제완료만
GROUP BY u.id, u.name
ORDER BY total DESC
LIMIT 5;
⚙️ 처리 단계 DB 엔진 내부 동작
📋
1. 파싱
SQL 문법 분석 → AST 생성
10ms
🧠
2. 옵티마이저
인덱스·조인 순서 최적화 (통계 기반)
25ms
⚙️
3. 실행
Storage Engine이 데이터 읽기
30ms
📊
4. 결과 반환
버퍼에 모아서 클라이언트에 전달
8ms
총 73ms
📤 출력
✓ 5 rows 4 cols 73ms
idnameorder_counttotal
42홍길동15450000
41김철수12380000
37이영희8215000
55박민수389000
12최지원00
💡 학습 포인트: 실제 DB는 파싱 → 옵티마이저 → 실행 → 반환 4단계를 거칩니다. 인덱스가 있으면 '옵티마이저' 단계가 수천배 빨라집니다.
🌐 본인 환경(PostgreSQL·MySQL 등)에서 직접 실행해 ms 차이를 체감해보세요.

💾 실전 2 — 월별 매출 (DATE_TRUNC + HAVING)

시간 집계 + HAVING 필터 패턴. 매출이 100만원 넘는 월만 표시.
📥 입력 DB 서버로 전송되는 쿼리
SELECT FROM: orders length: 328 chars
-- 입력: orders (created_at, total, status)
-- 처리: 월 단위 그룹화 + 결제완료 필터 + 100만 이상만
-- 출력: 월별 주문 수, 매출

SELECT
  DATE_TRUNC('month', created_at)::DATE AS month,
  COUNT(*) AS orders,
  SUM(total) AS revenue,
  ROUND(AVG(total)) AS avg_order
FROM orders
WHERE status = 'PAID'
GROUP BY month
HAVING SUM(total) > 1000000
ORDER BY month;
⚙️ 처리 단계 DB 엔진 내부 동작
📋
1. 파싱
SQL 문법 분석 → AST 생성
10ms
🧠
2. 옵티마이저
인덱스·조인 순서 최적화 (통계 기반)
25ms
⚙️
3. 실행
Storage Engine이 데이터 읽기
27ms
📊
4. 결과 반환
버퍼에 모아서 클라이언트에 전달
8ms
총 70ms
📤 출력
✓ 4 rows 4 cols 70ms
monthordersrevenueavg_order
2025-01-01142182000012817
2025-02-01188245000013031
2025-03-01215312000014511
2025-04-01276418000015145
💡 학습 포인트: 실제 DB는 파싱 → 옵티마이저 → 실행 → 반환 4단계를 거칩니다. 인덱스가 있으면 '옵티마이저' 단계가 수천배 빨라집니다.
🌐 본인 환경(PostgreSQL·MySQL 등)에서 직접 실행해 ms 차이를 체감해보세요.

💾 실전 3 — Window 함수 (사용자별 최근 주문 Top 3)

고급 패턴 — ROW_NUMBER + PARTITION BY 로 사용자마다 상위 N개 추출.
📥 입력 DB 서버로 전송되는 쿼리
SELECT FROM: orders length: 328 chars
-- 입력: orders
-- 처리: PARTITION BY user_id 로 사용자별 정렬 → 상위 3개
-- 출력: 각 사용자의 최근 주문 3건

WITH ranked AS (
  SELECT
    o.*,
    ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY created_at DESC
    ) AS rn
  FROM orders o
)
SELECT user_id, id AS order_id, total, created_at
FROM ranked
WHERE rn <= 3
ORDER BY user_id, rn;
⚙️ 처리 단계 DB 엔진 내부 동작
📋
1. 파싱
SQL 문법 분석 → AST 생성
10ms
🧠
2. 옵티마이저
인덱스·조인 순서 최적화 (통계 기반)
25ms
⚙️
3. 실행
Storage Engine이 데이터 읽기
30ms
📊
4. 결과 반환
버퍼에 모아서 클라이언트에 전달
8ms
총 73ms
📤 출력
✓ 5 rows 4 cols 73ms
user_idorder_idtotalcreated_at
421832450002025-04-25
421801320002025-04-22
421789280002025-04-19
411820180002025-04-24
411798550002025-04-21
💡 학습 포인트: 실제 DB는 파싱 → 옵티마이저 → 실행 → 반환 4단계를 거칩니다. 인덱스가 있으면 '옵티마이저' 단계가 수천배 빨라집니다.
🌐 본인 환경(PostgreSQL·MySQL 등)에서 직접 실행해 ms 차이를 체감해보세요.
💻 📌 실전 SQL 패턴 모음 (복사용 cheatsheet)
-- ============================================
-- 자주 쓰는 SQL 패턴 8선 (PostgreSQL 기준)
-- ============================================

-- 1) UPSERT (있으면 업데이트, 없으면 INSERT)
INSERT INTO users (email, name) VALUES ('[email protected]', '홍길동')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;

-- 2) keyset pagination (큰 OFFSET 대신)
-- ❌ 느림: SELECT * FROM posts ORDER BY id DESC OFFSET 100000 LIMIT 10;
-- ✅ 빠름:
SELECT * FROM posts WHERE id < :last_id ORDER BY id DESC LIMIT 10;

-- 3) 중복 제거 + 최신 1건만
SELECT DISTINCT ON (user_id) user_id, id, created_at
FROM orders ORDER BY user_id, created_at DESC;

-- 4) JSON 컬럼 조회 (Postgres jsonb)
SELECT id, metadata->>'theme' AS theme
FROM users WHERE metadata @> '{"premium": true}';

-- 5) 전문 검색 (Full-text)
SELECT * FROM articles
WHERE to_tsvector('korean', title || ' ' || body) @@ plainto_tsquery('Spring Boot');

-- 6) 안전한 카운트 (정확 vs 추정)
SELECT COUNT(*) FROM huge_table;                     -- 정확, 느림
SELECT reltuples FROM pg_class WHERE relname='huge_table';  -- 추정, 즉시

-- 7) Window 함수 — 누적 합·이동평균
SELECT date, sales,
       SUM(sales) OVER (ORDER BY date) AS cumulative,
       AVG(sales) OVER (ORDER BY date ROWS 6 PRECEDING) AS ma7
FROM daily_sales;

-- 8) Recursive CTE — 계층 구조 (조직도·카테고리 트리)
WITH RECURSIVE org AS (
  SELECT id, name, manager_id, 1 AS depth
  FROM employees WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.name, e.manager_id, o.depth + 1
  FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY depth, name;

EXPLAIN — 쿼리 실행 계획 보기

EXPLAIN 이 왜 필요한가

같은 결과를 내는 쿼리도 DB 가 내부적으로 어떻게 실행 하느냐에 따라 수십·수백 배 속도 차이. EXPLAIN 은 MySQL 옵티마이저의 결정 을 보여주는 도구.

사용법

sql
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';

결과:

code
+----+--------+-------+------+---------------+---------+---------+-------+------+
| id | select | table | type | possible_keys | key     | rows    | Extra |
+----+--------+-------+------+---------------+---------+---------+-------+------+
|  1 | SIMPLE | users | ref  | idx_email     | idx_email | 1     | NULL  |
+----+--------+-------+------+---------------+---------+---------+-------+------+

가장 중요한 컬럼 — typerows

type 등급 — 좋은 순서대로

1. const / system — 1건 확정 (PK 로 조회). 최고.
2. eq_ref — JOIN 시 PK·UNIQUE. 매우 좋음.
3. ref — 인덱스 사용. 좋음.
4. range — 범위 인덱스 (WHERE age > 20). 무난.
5. index — 인덱스 전체 스캔. 나쁨.
6. ALL풀 테이블 스캔. 극혐 — 인덱스 없는 상태.

type 컬럼에 ALL 이 나오면 인덱스를 만들거나 쿼리를 수정 하세요.

rows — 검사할 행 수의 예측

100만 행 테이블에서 rows: 1000000 → 풀 스캔. rows: 5 → 인덱스 적중. rows 가 작을수록 빠름.

EXPLAIN ANALYZE — 실제 실행 시간까지 (MySQL 8+)

sql
EXPLAIN ANALYZE SELECT ...;

실제로 실행하면서 단계별 소요 시간 출력. 느린 쿼리 디버깅의 핵심 도구.

실무 워크플로우

1. 느린 쿼리 로그 (slow_query_log) 에서 1초 이상 쿼리 수집
2. 각 쿼리에 EXPLAIN 실행
3. type=ALL 또는 rows 가 큰 쿼리 → 인덱스 추가 검토
4. EXPLAIN ANALYZE 로 개선 전후 비교

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "users · orders · products 3 테이블 설계 + FK + INDEX 까지 만들어줘"
  • "최근 7일 가입한 사용자 수를 일별로 집계하는 쿼리 작성해줘"
  • "이 쿼리에 EXPLAIN 붙여서 실행계획 해석해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

SQL 기초 — RDBMS·SELECT·WHERE·GROUP BY·JOIN - Database