sql 지금까지 정리된 글 목록
1. SQL 기본 흐름 — SELECT부터 조건까지
2. [SQL] 데이터를 숫자로 요약하기 — 집계와 그룹화
3. 잠깐 짚고 가기: 데이터 딕셔너리
4. [SQL] 테이블 구조 이해 & JOIN 준비 운동
5. JOIN 실전 — 테이블 연결하기

6. 복잡한 쿼리를 다루는 방법 — 서브쿼리와 CTE

7. 윈도우 함수 — 분석가의 필수요소

 

지금까지 정리한 글 정독하며 각 챕터별 중요 포인트 정리

- 주기적으로 진행하기 (1주일 단위) 문법을 복습하기보다 익숙해진다는 느낌으로 접근하기

- 특히 서브쿼리 많이 하기 (아직도 많이 햇갈림)

(어떨때? 왜? 사용하는지 익히기 위함)


추가 필요한 내용

CONCAT, SUBSTER [ (LV.2 24. 카테고리 별 상품 개수 구하기) ] (라이브 세션 6회차에 적혀있음)

IFFNULL COALESCE 내용 추가

GROUND, FLOOR, CEIL 내용 추가

COUNT(DISTINCT)와 GROUP_CONCAT

년, 월, 일 추출 방법 (YEAR, MONTH, DAY), DATE_FORMAT

REGEXP [(LV.2  23. 중성화 여부 파악하기, LV2. 30. 자동차 종류 별 특정 옵션이 포함된 자동차 수 구하기 )]

WITH RECURSIVE[ (LV.5 73. 입양 시각 구하기(2)) ]

 

추가로 공부해야 할 내용

1. 데이터 변환/함수 활용 세부 파트 

2. 실습 문제 + 해설 파트 (코드카타로 대체 고려중)

3. 자주 틀리는 포인트/주의 포인트 정리

5. 고급 SQL

- 인덱스 및 기본 성능 최적화
- 집합 연산(UNION vs INTERSECT vs EXCEPT) 세부
- 뷰(View)와 저장 쿼리

 


 

python 지금까지 정리된 글 목록

1. 입출력과 변수형

2. 조건문과 반복문

3. 자료형 STEP 1. 리스트

4. 자료형 STEP 2. 튜플

5. 자료형 STEP 3. 딕셔너리

6. 함수와 모듈

 

추가 작성 필요

클래스

 

지금까지 정리한 글 정독하며 각 챕터별 중요 포인트 정리

리스트, 튜플, 딕셔너리.

조건문과 반복문

가장 중요한 파트니 집중적으로 시행할것

 

파이썬 코드카타 문제들 다시한번 보면서 풀어보기

+ 수도코드 및 실행순서 작성하면서 생각하면서 풀기

(왜 이렇게 작동했는지 이해하기 위함)

 

데이터 전처리 내용 예습 가능하면 한번 예습하기

 

1. 왜 윈도우 함수가 중요한가

SQL을 배우면서 대부분 이런 흐름을 거친다.

• WHERE, GROUP BY, JOIN은 어느 정도 익숙해짐

• 집계 함수(SUM, AVG, COUNT)도 쓸 수 있음

 

그런데 “행을 유지하면서 분석 지표를 같이 보고 싶다”는 순간 막히기도 한다.

예를 들면 이런 요구사항이다.

• 각 직원의 급여와 함께, 같은 부서 평균 급여를 같이 보고 싶다
• 매일 매출 데이터에서, 누적 매출과 일별 증감을 보고 싶다
• 동일 그룹 내에서 순위를 매기고 싶다

이때는 기존의 GROUP BY는 한계가 있다.

 

GROUP BY → 행을 줄이고, 집계 결과만 남긴다

분석에서는 오히려 → 행은 그대로 두고 분석용 컬럼을 추가하고 싶다.

이때 이 역할을 수행 하는 것이 바로 윈도우 함수(Window Function)다.


2. 윈도우 함수의 핵심 개념

집계를 하되, 행을 줄이지 않는다.

 

기존의 집계함수

SELECT department, AVG(salary)
FROM employees
GROUP BY department;

 

부서별 평균은 나오지만

개별 직원 정보는 사라진다

 

윈도우 함수

SELECT
    employee_id,
    department,
    salary,
    AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary
FROM employees;

 

직원 한 명당 한 행 유지

같은 부서의 평균 급여를 옆에 붙여서 보여준다


3. OVER(PARTITION BY ...) 구조 이해하기

집계함수() OVER (PARTITION BY 기준컬럼)

윈도우 함수는 항상 OVER()와 함께 사용된다.

 

수도코드로 보면 다음과 같다.

전체 테이블을 가져온다
PARTITION BY 기준으로 그룹을 나눈다
각 그룹 안에서 집계를 계산한다
결과를 각 행 옆에 붙인다

중요한 점은 GROUP BY처럼 행을 합치지 않는다는 것이다.


4. 그룹 합계 / 평균 구하기

그룹 합 / 개수 / 평균을 행을 유지한 채로 붙이기

• COUNT/SUM/AVG OVER

• 집계 결과를 만들되, 행을 줄이지 않는다

집계함수(...) OVER (
PARTITION BY ...
ORDER BY ...
)

PARTITION BY → 어떤 기준으로 “그룹을 나눌지”
ORDER BY → 파티션 안에서의 순서 (선택)

PARTITION BY로 나눈 “그룹 안에서” 집계함수(SUM / COUNT / AVG)가 계산된다

 

예제: 부서별 평균 급여를 직원별로 확인

직원 데이터를 하나씩 본다
같은 부서 직원들만 묶는다
그 부서의 평균 급여를 계산한다
각 직원 행에 평균 급여를 붙인다

 

SQL 예제

SELECT
    employee_id,
    department,
    salary,
    AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary
FROM employees;

실행 순서 (Line by Line 개념)

1. employees 테이블 전체 조회
2. department 기준으로 파티션 생성
3. 각 파티션 내에서 AVG(salary) 계산
4. 계산된 평균을 각 행에 출력

5. 순번과 순위 함수

랭킹 계열 윈도우 함수 = 그룹 안에서 “줄 세우기”

• ROW_NUMBER / RANK / DENSE_RANK

🚨이 번호는 PARTITION BY로 묶인 그룹 안에서만 의미가 있다

랭킹함수() OVER (
    PARTITION BY 그룹기준
    ORDER BY 정렬기준
)

5-1. ROW_NUMBER()

무조건 1,2,3… 번호 (동점이어도 나눠짐)

ROW_NUMBER() OVER (
PARTITION BY ...
ORDER BY ...
)

개념

• 파티션 내에서 각 행에 고유한 번호를 부여

• 동점이 있어도 번호는 무조건 나뉜다

• 결과는 항상: 1, 2, 3, 4, …

→순위 보단 순번에 가까움

 

⭐ 포인트

• ORDER BY가 없으면 결과가 비결정적 (실행할 때마다 달라질 수 있음)

• “가장 최신 1건”, “각 그룹의 대표 행 1개” 뽑을 때 자주 사용

 

SELECT
    employee_id,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
FROM employees;

수도코드

부서별로 나눈다
급여 높은 순으로 정렬한다
위에서부터 번호를 매긴다

5-2. RANK

동점이면 같은 순위, 다음 순위는 건너뜀(갭)

RANK() OVER (
PARTITION BY ...
ORDER BY ...
)

개념적으로 “등수 개념에 가까움” (1등이 2명이면 다음은 3등)


 

5-3. DENSE_RANK

동점이면 같은 순위, 다음 순위는 안 건너뜀(촘촘)

DENSE_RANK() OVER (
PARTITION BY ...
ORDER BY ...
)

개념적으로 “그룹 내 순서 레벨” (1 → 2 → 3 처럼 촘촘하게 증가)


표로 살펴보기

상황 ROW_NUMBER RANK DENSE_RANK
동점 처리 무시 같은 순위 같은 순위
다음 순위 연속 건너뜀 연속
대표 1행 최적 x x

“각 그룹에서 1행만 뽑고 싶다”
→ ROW_NUMBER()


“순위 자체가 의미 있는 경우”

→ RANK()


“등급 / 레벨처럼 연속된 순위가 필요”
→ DENSE_RANK()

 

🚨 햇갈리는 포인트! 🚨

전체 테이블에서 등수를 정하는게 아님!

PARTITION BY 로 나눠진 컬럼 내부에서 등수를 매기는 것!

 

머리속에서 이렇게 이해하기

user A: 1, 2, 3 ...
user B: 1, 2 ...
user C: 1 ...

 

만약 PARTITION BY를 쓰지 않고 ROW_NUMBER() OVER (ORDER BY created_at DESC)를 쓰면?

전체 순위가 나옴.


6. 누적합 (Cumulative Sum)

누적합/비율/최근 N개 평균 (SUM/AVG OVER + ORDER BY + Frame)

• 행의 위치에 따라 계산에 포함되는 범위가 달라진다

 

어떨때 사용하나?

• 하루하루 매출이 쌓여서 지금까지 총 매출

• 이 주문이 전체 매출 중 몇 %

• 최근 3일 평균 매출

 

 

누적합: 프레임(Frame)

ORDER BY가 있는 윈도우 집계에는 기본 Frame이 자동 적용된다

ROWS BETWEEN [시작 지점] AND [끝 지점]

 

⭐ Frame 문법정리

Frame 의미
UNBOUNDED PRECEDING 맨 처음부터
CURRENT ROW 현재 행
N PRECEDING 현재 기준 앞의 N행
N FOLLOWING 현재 기준 뒤의 N행

 

-- 누적합 형태
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

개념

• “그룹의 첫 행 ~ 현재 행까지 누적” 이라는 뜻

• 물리적인 행 기준
• "앞에서부터 지금 이 행까지"를 명확히 보장

→ 누적합 = ROWS 프레임을 떠올리자!

 

비유
시험 성적을 한 번 볼 때마다
지금까지 받은 점수를 전부 더한 총점

 

생각의 흐름

1. PARTITION BY → 누구 기준?
2. ORDER BY → 어떤 순서?
3. Frame → 몇 개를 볼까?

 

문법 형태 예시

함수(...) OVER (
PARTITION BY ...
ORDER BY ...
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)

6-1. 각 행의 비율 계산

개념

• 한 행의 값은 그대로 둔다
• 같은 그룹의 전체 합계를 윈도우 함수로 붙인다
• 둘을 나눈다

🚨 GROUP BY는 행이 줄어들기 떄문에 불가능

value / SUM(value) OVER (PARTITION BY group_key)

이 행의 값 = value
이 행이 속한 그룹의 전체합= SUM(value) OVER (PARTITION BY group_key)

 


7. LAG / LEAD — 이전 값과 다음 값

“이전/다음 행 비교” (LAG / LEAD) + NULL 처리(IFNULL/COALESCE)

 

어떨때 사용하나?

• 이번 달 매출이 지난달보다 얼마나 늘었나?

• 이 로그의 이전 상태는 뭐였지?

• 오늘 점수가 어제보다 올랐나 떨어졌나?

공통점?

“내 옆 행(이전/다음)과 비교”

 

GROUP BY 나 집계(SUM/COUNT)나 순위(ROW_NUMBER, RANK, DENSE_RANK)와 달리,
LAG / LEAD는 행 자체를 이동해서 참조하는 함수


 

7-1) LAG / LEAD의 개념 졉근

LAG(값)

• 현재 행 기준 → 이전 행 값 가져오기
• “어제 값”, “이전 상태”, “직전 이벤트”를 볼 때 사용

 

LEAD(값)

• 현재 행 기준 → 다음 행 값 가져오기
• “다음 값”, “다음 상태”, “이후 이벤트”를 볼 때 사용

LAG(컬럼, 몇 칸 전, 기본값) OVER (
    PARTITION BY ...
    ORDER BY ...
)

LEAD(컬럼, 몇 칸 후, 기본값) OVER (
    PARTITION BY ...
    ORDER BY ...
)

 

LAG(값) : 내 바로 “이전 행”의 값을 가져온다
LEAD(값) : 내 바로 “다음 행”의 값을 가져온다

 

ORDER BY

→ 는 사실상 필수(“전/후” 기준이 필요)

 

⭐ORDER BY는 사실상 필수

LAG / LEAD에서 ORDER BY는 선택이 아니라 의미를 결정하는 기준
ORDER BY가 없을 경우:
“이전/다음”의 기준이 없다
결과는 비결정적 (매 실행마다 달라질 수 있음)

 

⭐ NULL이 생기는 이유 (중요)

LAG / LEAD는 구조적으로 NULL을 만들 수밖에 없다.

• 파티션의 첫 행 → LAG = NULL

• 파티션의 마지막 행 → LEAD = NULL

이 NULL은 “데이터가 없는 것”이 아닌

“참조할 이전/다음 행이 구조적으로 존재하지 않음” 을 의미

→반환값(default)를 설정하거나 IFNULL, COALESCE을 사용하자


 

추가내용

그룹별 Top N 데이터 추출하기

분석 SQL을 어느 정도 다루다 보면 반드시 마주치는 요구사항이 있다.

• 카테고리별 상위 상품 3개
• 부서별 급여 Top 2 직원
• 사용자별 최근 주문 1건

• 날짜별 매출 상위 N건

이런 문제는 모두 같은 구조를 가진다.

 

그룹마다 상위 N개 행만 남기기 (Top N per Group)

그룹별 Top N 문제는 다음 3가지 요소로 구성된다.

  1. 그룹을 나눈다 → PARTITION BY
  2. 그룹 안에서 정렬한다 → ORDER BY
  3. 순번을 붙이고 잘라낸다 → ROW_NUMBER / RANK + 필터링

즉, 정렬 + 순번 + 필터 이 조합이 항상 등장한다.


접근 방법 정리

처음 문제를 보면 바로 SQL을 쓰려고 하지 말고 다음 질문을 순서대로 던져보자

 

Step 1. 무엇 기준으로 “Top”인가?

가장 먼저 정렬 기준을 명확히 한다.

• 가격 기준 Top?

• 날짜 기준 최신?

• 점수 기준 상위?

이 기준이 ORDER BY가 된다.

 

Step 2. 그룹은 무엇인가?

그 다음 “그룹마다”의 기준을 정한다.

• 부서별?

• 사용자별?

• 카테고리별?

이 기준이 PARTITION BY가 된다.

 

Step 3. 순번을 붙인다.

그룹 내부에서 정렬된 상태로 각 행에 순번을 붙인다.

ROW_NUMBER() OVER (
    PARTITION BY 그룹컬럼
    ORDER BY 기준컬럼 DESC
)

이때 보통 별칭으로 rn을 사용한다.

 

Step 4. N까지만 남긴다

마지막으로 순번을 기준으로 잘라낸다.

WHERE rn <= N
 

 

이것만 기억하자!

 

  • 그룹 안에서 순위를 매긴다
  • 그 순위를 WHERE로 잘라낸다

ROW_NUMBER vs RANK 선택 기준

  • ROW_NUMBER → 무조건 1, 2, 3… (동점 무시)
  • RANK → 동점 허용, 순위 건너뜀

 

문제를 보고 생각하자

“지금 내가 필요한 건 ‘행을 늘리는 결합’인가? 아니면 ‘기준값/판단용 결과’를 먼저 만드는 건가?”

 

 

JOIN을 써야 하는 상황?

JOIN은 언제 쓰나?

“이 테이블의 각 행에 다른 테이블의 정보를 붙이고 싶을 때”

 

특징

  • 결과 행 수가 늘어나거나 유지됨
  • 테이블 ↔ 테이블 연결
  • “정보 확장” 느낌

예시

-- 주문 + 고객 이름
SELECT o.order_id, u.name
FROM orders o
JOIN users u ON o.user_id = u.user_id;

“주문에 고객 정보를 붙인다”


서브쿼리를 써야 하는 상황?

 

기준값’을 먼저 만들어야 할 때

예시
“전체 평균 급여보다 많이 받는 직원”

 

수도코드

1. 전체 평균 급여 계산
2. 그 값과 직원 급여 비교

 

SELECT *
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
);

조건용 집계 결과’가 필요할 때

예시

"거래 금액이 70만 원 이상인 사용자”

1. 사용자별 거래 금액을 계산한다
2. 70만 원 이상인 사용자만 고른다
3. 그 사용자 정보가 필요하다

 


서브쿼리 vs JOIN을 가르는 결정적 질문 3개

 

질문 1: 지금 필요한 게 집계 결과(AVG, SUM, COUNT) 인가?

YES → 서브쿼리

 

질문 2: 그 집계 결과를 조건이나 기준으로 써야 하는가?

YES → 서브쿼리

 

질문 3: 그 결과를 행처럼 다시 써야 하는가?

YES → FROM 서브쿼리


가장 중요한점!

현재 내가 집중하고 있는 포인트

“이 문제도 서브쿼리로 풀 수 있지 않을까?”

 

대신 이렇게 생각하자!

“이 문제에서 서브쿼리가 자연스러운가?


앞으론 이렇게 연습하기

 

  • 집계가 필요한가?
  • 집계 결과가 조건인가?
  • 그 결과를 다시 써야 하나?

 

 

[SQL] 복잡한 쿼리를 다루는 방법 — 서브쿼리와 CTE

 

[SQL] 복잡한 쿼리를 다루는 방법 — 서브쿼리와 CTE

1. 서브쿼리란 무엇인가?서브쿼리는쿼리 안에서 실행되는 또 다른 쿼리다.SQL에서는 서브쿼리를 이용해 어떤 값을 먼저 계산한 뒤, 그 결과를 바깥 쿼리에서 조건이나 기준으로 사용한다. 특징•

papago9211.tistory.com

과 내용 합침

 

문제를 보다보면 서브쿼리를 사용해아할 문제들이 꽤있다.

하지만 풀다보면 잘 안되기도 하고, 머리가 멈춰서 시간이 오래걸릴때가 많다.

 

나는 지금 서브쿼리에 대해 무엇을 잘못 생각하고 있는걸까?

개념을 다시 짚고 넘어가자


서브쿼리란...

쿼리 안에서 실행되는 또 다른 쿼리라고 정의할수 있다.

 

⭐중요 포인트⭐

대부분의 상황에선 서브쿼리가 먼저 작동한다.

SELECT
	컬럼1,
	(SELECT 집계함수(컬럼)
	FROM 테이블
	WHERE 조건) AS 기준값
FROM 테이블;

SELECT 컬럼1
FROM 테이블;
WHERE 연산자 (SELECT 집계함수(컬럼)
		FROM 테이블
		WHERE 조건) AS 기준값

와 비슷한 형태를 지닌다.

사용되는 위치에 따라서 역할이 달라지기도 한다.

SELECT 절 : 기준값을 출력에 같이 보여줄 때
WHERE 절  : 조건 비교를 위한 값이나 목록이 필요할 때
FROM 절   : 중간 결과를 테이블처럼 사용할 때

EXISTS절: 존재 여부를 판정할때

SELECT 절에서 사용될 경우...

각 행 옆에 기준이 되는 값(평균, 합계 등) 을 같이 보여주고 싶을 때 사용한다.

“기준값을 계산해서 같이 보여주고 싶을 때”

형태

SELECT
    컬럼1,
    (SELECT 집계함수(컬럼)
     FROM 테이블
     WHERE 조건) AS 기준값
FROM 테이블;

 

  • 결과는 항상 한 값
  • 각 행 옆에 비교 기준, 평균, 합계 등을 붙일 때

 

 

예시문제 1. 직원의 이름과 급여, 비교를 위해 평균 급여를 출력하라.

수도코드로 보기

전체 직원의 평균 급여를 구한다.
각 직원의 이름, 급여를 조사한다.
조회할 때 평균 급여를 같이 보여준다.

 

SELECT
    name,
    salary,
    (SELECT AVG(salary) FROM employees) AS avg_salary
FROM employees;

 

실행 순서 (Line by Line)

1. 서브쿼리 실행  → 평균 급여 1개 값 생성
2. employees 테이블 한 행씩 출력
3. 각 행마다 평균급여 값을 그대로 붙임

 

WHERE 절에서 사용될 경우...

조건에 쓸 기준값이 필요할 때, 조건을 비교해야 할 때 사용한다.

# 값이 1개일 때
WHERE 컬럼 = (SELECT ...)

# 값이 여러개일 때
WHERE 컬럼 IN (SELECT ...)

단일값 예시문제 1. 급여가 평균보다 큰 사람을 출력하라

수도코드로 보기

전체 직원의 평균 급여를 계산한다.
그 결과를 하나의 테이블로 구성한다.
평균급여보다 급여가 많은 사람을 구한다.

 

SELECT name, salary
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
);

 

실행 순서 (Line by Line)

1. 서브쿼리 실행  → 평균 급여 1개 값 생성
2. employees 테이블 첫 번째 행 조회
3. salary > 평균 급여 비교
4. 조건이 참이면 출력, 거짓이면 제외
5. 모든 행에 대해 반복

 

여러 값 예시문제 2. 서울 소재 부서 소속 직원 조회 문제

수도코드로 보기

서울에 있는 부서의 id 목록을 구한다
그 부서에 속한 직원만 조회한다

 

SELECT *
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = '서울'
);

 

실행 순서 (Line by Line)

1. 서브쿼리 실행 → 서울에 위치한 부서의 department_id 목록 생성
2. employees 테이블에서 행 하나씩 조회
3. department_id가 목록 안에 있는지 확인
4. 있으면 출력, 없으면 제외

 


FROM 절에서 사용될 경우...

중간 결과를 테이블처럼 사용하고 싶을 때 사용한다.

SELECT 컬럼 1, 컬럼2 
FROM ( 
	SELECT 컬럼 1, 컬럼 2
    	FROM 테이블
        GROUP BY 묶을 컬럼) AS 서브쿼리명;

예시문제 1. 부서별 평균 급여 기준 필터링 문제

 

수도코드로 보기

부서별 평균 급여를 계산한다
이 결과를 하나의 임시 테이블로 만든다
평균 급여가 3000 이상인 부서만 조회한다

 

SELECT department, avg_salary
FROM (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
) t
WHERE avg_salary >= 3000;

 

실행 순서 (Line by Line)

1. FROM 절 안의 서브쿼리 실행→ 부서별 평균 급여 테이블 생성
2. 생성된 임시 테이블을 t로 이름 지정
3. t 테이블에 대해 WHERE 조건 적용
4. 조건을 만족하는 행 출력

EXISTS 서브쿼리문

WHERE EXISTS ( #not exisits는 없는것을 판별
    SELECT 1
    FROM 테이블
    WHERE 조건
)

 

예시문제 1. 주문 이력이 있는 고객 조회 문제

수도코드로 보기

이 고객에 대한 주문이 존재하는지 확인한다
주문이 하나라도 있으면 고객을 출력한다

 

SELECT *
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id
);

 

실행 순서 (Line by Line)

customers 테이블의 각 행을 하나씩 확인
해당 customer_id로 orders에 행이 있는지 검사
하나라도 있으면 TRUE → 고객 출력

내용정리

SELECT 절 : 기준값을 출력에 같이 보여줄 때
WHERE 절  : 조건 비교를 위한 값이나 목록이 필요할 때
FROM 절   : 중간 결과를 테이블처럼 사용할 때

EXISTS절: 존재 여부를 판정할때

 

 


상관 서브쿼리 (Correlated Subquery)

지금까지 봤던 서브쿼리는 대부분 이러했다.

• 서브쿼리 혼자 실행 가능

• 한 번 실행 → 결과를 바깥 쿼리가 사용

허나, 상관 서브쿼리는 다르다.

 

상관 서브쿼리는 바깥 쿼리의 한 행마다 서브쿼리가 다시 실행된다. 

 

그렇다면 언제 사용하나?

“각 행마다 기준이 달라질 때”

 

  • 각 부서에서 평균보다 급여가 높은 직원
  • 각 고객의 주문 평균 금액보다 큰 주문
  • 각 상품의 카테고리 평균보다 비싼 상품

예제 문제: 각 부서에서 자기 부서 평균 급여보다 많이 받는 직원 조회

수도코드로 보기

직원 한 명을 선택한다
이 직원이 속한 부서의 평균 급여를 계산한다
직원 급여가 그 평균보다 큰지 비교한다
크면 출력한다
다음 직원으로 이동한다

 

SELECT name, salary, department
FROM employees e
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
    WHERE department = e.department
);

 

실행 순서 (Line by Line)

1. employees 테이블에서 첫 번째 직원 e 선택
2. 서브쿼리 실행 → 이 직원의 부서 평균 급여 계산
3. e.salary > 부서 평균 급여 비교
4. 조건이 참이면 출력
5. 다음 직원으로 이동하여 반복

 


CTE (WITH 절)

 

CTE는 서브쿼리와 목적은 같으며 차이점은...

서브쿼리를 위에서 이름 붙여서 미리 정의한다. 때문에 읽기 쉽고, 재사용도 가능하다.

그렇다면 언제 사용하나?

 

  • FROM 절 서브쿼리가 길어질 때
  • 같은 서브쿼리를 여러 번 써야 할 때
  • 실행 순서를 명확히 보여주고 싶을 때

예제 문제: 부서별 평균 급여를 구한 뒤 평균 급여가 3000 이상인 부서 조회

수도코드로 보기

부서별 평균 급여 테이블을 만든다
이 테이블에 이름을 붙인다
그 결과에서 평균 급여가 3000 이상인 부서를 조회한다

 

WITH dept_avg AS (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
)
SELECT department, avg_salary
FROM dept_avg
WHERE avg_salary >= 3000;

 

실행 순서 (Line by Line)

1. WITH 절 실행 → 부서별 평균 급여 테이블 생성 → dept_avg라는 이름으로 저장
2. 메인 SELECT 실행 FROM dept_avg
3. WHERE 조건 적용
4. 결과 출력

 

 


개념적 이해가 부족한것은 아님.

JOIN = 테이블과 테이블을 연결 그리고 연결의 기준은 ON, 보통 PK ↔ FK.

INNER JOIN = 교집합
  • ON 조건에 매칭되는 것만
  • 한쪽이라도 없으면 결과에서 사라짐

LEFT JOIN = 왼쪽 기준 유지
  • 왼쪽 테이블: 무조건 남김
  • 오른쪽 테이블: 매칭되면 붙고, 없으면 NULL

해당 개념들은 머리속에 박혀있는 상태,

하지만 문제를 볼때마다 어떻게 해야할지, join을 써야하는지 생각이 제대로 되지 않음

 

고민한 결과...

JOIN이 어려운 이유는 “문법”도 아니고 “INNER / LEFT 구분”도 아니였다.
진짜 이유는 “결과가 어떻게 생길지 머릿속에 안 그려짐”

 

이 JOIN을 하면 행(row)이 몇 개 나올지..
왜 늘어났는지 / 왜 줄었는지...
내가 보고 싶은 단위가 학생인지 / 신청인지 / 결제인지...

이러한 판단이 JOIN을 치기 전까지 제대로 서지 않는다.

 


JOIN의 중요 포인트


① 기준 테이블을 언제 정해야 하는지

기준 테이블은 JOIN 종류가 아니라 “내가 보고 싶은 결과”가 정한다

 

때문에...

내가 보고 싶은 최종 행 단위는 뭐지?를 항상 생각하고, 결정하고, 적어두자
학생 1행?
신청 1행?
결제 1행?

나는, 문제는 뭘 보고 싶은건지 생각하자!

길게 걸리더라도 반드시 할것!


② 1:N 관계에서 행이 늘어나는 이유

예를 들어:

학생 1명
수강신청 3건

JOIN시 결과에서 학생 정보가 3번 반복됨

이걸 머리로는 아는데,
“아, 그래서 이 쿼리는 위험하구나 / 안전하구나” 판단이 안 섬

 

 

해결책!

지금 JOIN하면 항상 질문하면서, 생각하면서 풀기

?? “이 쿼리의 기준 테이블은 누구지?”
?? “기준 테이블 1행이 몇 행으로 불어날 수 있지?”

  • 1:1 → 안전
  • 1:N → 늘어남 (정상인지 판단)

만약 늘어난다면... 줄이는 방법도 생각하기

?? GROUP BY로 묶으면 줄어드나?
?? 어떤걸로 묶어야 하나?


③ ON이랑 WHERE가 섞이기 시작할 때

특히 LEFT JOIN에서:
ON: “어떻게 붙일지”
WHERE: “결과에서 뭘 버릴지”

이게 섞이면:
“어? LEFT JOIN 했는데 왜 결과가 사라지지?”

이 순간 생각이 과부화 됨

 

 

해결책!

ON과 WHERE에 대해 다시한번 생각하기

❗ ON 조건을 만족하지 못해도 LEFT JOIN은 왼쪽 행을 살린다 ❗

  • ON → 테이블을 어떻게 연결할지 →  ON은 붙이는 조건
  • WHERE → 연결된 결과에서 무엇을 제거할지 →  WHERE는 거르는 조건

❗ 중요 ❗
못 붙이면 NULL 붙인다
행 자체를 버리지는 않는다 (LEFT JOIN이니까)

 

 

먼저 생각하고 나면 on과 where를 어떻게 사용할지 감이 잡힐것이다!


다음 코드카타 때 해당 흐름들을 한문제씩 사용해보기!

1. 서브쿼리란 무엇인가?

서브쿼리는 쿼리 안에서 실행되는 또 다른 쿼리다.

SQL에서는 서브쿼리를 이용해 어떤 값을 먼저 계산한 뒤, 그 결과를 바깥 쿼리에서 조건이나 기준으로 사용한다.

 

특징

• 항상 SELECT로 시작

• 괄호 ()로 감싸서 사용

• 바깥 쿼리보다 먼저 실행되는 경우가 많음

• 결과는 바깥 쿼리의 → 조건 / 비교 대상 / 값으로 사용됨

 

사용하는 이유?

“조건을 바로 쓸 수 없고, 먼저 계산하거나 골라내야 할 때”

• 비교 기준이 다른 테이블에 있을 경우

• 먼저 계산해야 할 값이 있는 경우

• 특정 조건에 맞는 값이 필요로 할때

• 요약된 결과를 기준으로 판단해야 할 경우

서브쿼리는 항상 이 값을 먼저 알아야 다음 단계를 진행할 수 있다는 상황에서 등장한다.

 

수도코드로 이해하기

먼저 기준이 되는 값을 계산한다
그 값을 이용해 바깥 쿼리를 실행한다 

서브쿼리는 사용되는 위치에 따라 역할이 바뀌기도 한다.

• SELECT 절 : 기준값을 출력에 같이 보여줄 때

• WHERE 절  : 조건 비교를 위한 값이나 목록이 필요할 때

• FROM 절   : 중간 결과를 테이블처럼 사용할 때

• EXISTS: 존재 여부를 판정할때


2. SELECT 절에서 쓰는 서브쿼리

각 행 옆에 기준이 되는 값(평균, 합계 등) 을 같이 보여주고 싶을 때 사용한다.

SELECT
    컬럼1,
    (SELECT 집계함수(컬럼)
     FROM 테이블
     WHERE 조건) AS 기준값
FROM 테이블;

결과는 항상 한 값만 나와야 한다. (하나의 행만, 때문에 집계 함수를 사용)

각 행 옆에 비교 기준, 평균, 합계 등을 붙일 때 사용된다.

 

예시문제: 직원의 이름과 급여, 비교를 위해 평균 급여를 출력하라.

수도코드로 보기

전체 직원의 평균 급여를 구한다.
각 직원의 이름, 급여를 조사한다.
조회할 때 평균 급여를 같이 보여준다.

 

SELECT
    name,
    salary,
    (SELECT AVG(salary) FROM employees) AS avg_salary
FROM employees;

 

실행 순서 (Line by Line)

1. 서브쿼리 실행  → 평균 급여 1개 값 생성
2. employees 테이블 한 행씩 출력
3. 각 행마다 평균급여 값을 그대로 붙임

 


3. WHERE 절에서 쓰는 서브쿼리

조건에 쓸 기준값이 필요할 때, 조건을 비교해야 할 때 사용한다.

# 값이 1개일 때 (행이 1개)
WHERE 컬럼 = (SELECT ...)

# 값이 여러개일 때 (행이 여러개)
WHERE 컬럼 IN (SELECT ...)

값이 1개일때, 값이 여러개일때로 나눠진다.

이때의 값은 행(row)를 의미하며, 만약 컬럼이 여러개인 경우 WHERE절이 아닌 FROM절에 서브쿼리를 사용해야 한다. 

 

단일값 예시문제: 급여가 평균보다 큰 사람을 출력하라

수도코드로 보기

전체 직원의 평균 급여를 계산한다.
그 결과를 하나의 테이블로 구성한다.
평균급여보다 급여가 많은 사람을 구한다.

 

SELECT name, salary
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
);

 

실행 순서 (Line by Line)

1. 서브쿼리 실행  → 평균 급여 1개 값 생성
2. employees 테이블 첫 번째 행 조회
3. salary > 평균 급여 비교
4. 조건이 참이면 출력, 거짓이면 제외
5. 모든 행에 대해 반복

 

여러 값 예시문제: 서울 소재 부서 소속 직원 조회 문제

수도코드로 보기

서울에 있는 부서의 id 목록을 구한다
그 부서에 속한 직원만 조회한다

 

SELECT *
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = '서울'
);

 

실행 순서 (Line by Line)

1. 서브쿼리 실행 → 서울에 위치한 부서의 department_id 목록 생성
2. employees 테이블에서 행 하나씩 조회
3. department_id가 목록 안에 있는지 확인
4. 있으면 출력, 없으면 제외

 

4. FROM 절 에서 쓰는 서브쿼리

원본 테이블을 그대로 쓰지 않고, 서브쿼리를 미리 가공한 결과를 새로운 테이블처럼 사용할때 쓰는 방식이다.

즉, “값”이 아니라 “테이블”을 만드는 서브쿼리이다

SELECT 컬럼 1, 컬럼2 
FROM ( 
	SELECT 컬럼 1, 컬럼 2
    	FROM 테이블
        GROUP BY 묶을 컬럼) AS 서브쿼리명;

예시문제: 부서별 평균 급여 기준 필터링 문제

 

수도코드로 보기

부서별 평균 급여를 계산한다
이 결과를 하나의 임시 테이블로 만든다
평균 급여가 3000 이상인 부서만 조회한다

 

SELECT department, avg_salary
FROM (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
) t
WHERE avg_salary >= 3000;

 

실행 순서 (Line by Line)

1. FROM 절 안의 서브쿼리 실행→ 부서별 평균 급여 테이블 생성
2. 생성된 임시 테이블을 t로 이름 지정
3. t 테이블에 대해 WHERE 조건 적용
4. 조건을 만족하는 행 출력

 

4-1. JOIN 옆에 사용하는 서브쿼리

FROM 서브쿼리가

FROM절에서 서브쿼리를 사용해 미리 가공한 결과를 하나의 테이블처럼 사용하는 방식이라면

 

JOIN 서브쿼리는 FROM 절에 있는 기본 테이블에 서브쿼리를 JOIN 하는 방식을 알아볼것이다.

이 방식 역시 "값"이 아닌 "테이블"을 만드는 서브쿼리이지만, 기존 테이블과의 관계를 유지한 채로 필요한 정보를

추가적으로 사용할때 사용한다.

SELECT 컬럼들
FROM 테이블 A
JOIN (
    SELECT 컬럼들
    FROM 테이블 B
    GROUP BY 컬럼
) 서브쿼리명
ON A.컬럼 = 서브쿼리명.컬럼;

# join은 용도에 따라 INNER, LEFT로 나눠짐

서브쿼리가 JOIN 대상 테이블 역할을 하고 ON 절을 통해 기존 테이블과 연결된다.

 

예시문제: 부서별 평균 급여와 직원 정보 함께 조회

수도코드로 보기

부서별 평균 급여를 계산한다.
이 결과를 임시 테이블로 구성한다.
직원 테이블과 부서 기준으로 JOIN한다.
직원정도 + 부서 평균 급여를 함께 조회한다.

 

SELECT e.name,
       e.department,
       e.salary,
       d.avg_salary
FROM employees e
JOIN (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
) d
ON e.department = d.department;

 

실행 순서 (Line by Line)

1. FROM 절 안의 JOIN 서브쿼리 실행→ 부서별 평균 급여 테이블 생성
2. 생성된 임시 테이블을 d로 이름 지정
3.  employees 테이블(e)과 d 테이블을 department 기준으로 JOIN
4. JOIN된 결과에서 SELECT 컬럼 출력

 

FROM절 서브쿼리와 JOIN서브쿼리의 차이점

공통점

• 둘 다 "값"이 아닌 "테이블을 만드는 서브쿼리문

• GROUP BY, 집계 결과를 다뤄야 할때 자주 사용됨

• 임시 테이블로써 동작함

 

차이점

구분 FROM절 서브쿼리 JOIN 서브쿼리
목적 결과 자체를 필터링 기존 테이블에 정보를 추가
중심 테이블 서브쿼리 결과 원본 테이블
사용 위치 FROMW 단독 JOIN 옆
활용 집계 결과를 필터링 집계 결과 결합

 

사용 타이밍을 자세하게

FROM절은 언제?

• 집계 결과 자체를 다시 조건으로 걸고 싶을 때

• “이 결과를 하나의 테이블로 보고 싶다”는 느낌일 때

예시

• 부서 평균 급여가 3000 이상인 부서만 조회

• 카테고리별 평균 가격 상위 그룹 추출

 

JOIN은 언제?

• 기존 테이블의 행을 유지하면서 집계된 정보를 옆에 붙이고 싶을 때

예시

• 직원 + 부서 평균 급여

• 주문 + 고객별 총 주문 금액

 

포인트!

FROM 절 서브쿼리는 결과 자체가 주인공 → “이 결과를 필터링하고 싶은가?”

JOIN 서브쿼리는 기존 테이블이 주인공 → “아니면 옆에 붙이고 싶은가?”

로 판단하기


5. EXISTS / NOT EXISTS — 존재 여부로 판단하기

EXISTS는 값을 비교하지 않고, 행이 존재하는지만 판단한다.

  값이 필요한가? → NO

  있냐 없냐만 중요한가? → YES

 

-- EXISTS
SELECT *
FROM 테이블 A
WHERE EXISTS (
	SELECT 1
	FROM 테이블 B
	WHERE 조건
); -- “이 안쪽 쿼리가 한 줄이라도 나오면 TRUE”

-- NOT EXISTS
SELECT *
FROM 테이블 A
WHERE NOT EXISTS (
	SELECT 1
	FROM 테이블 B
	WHERE 조건
);

 

예시문제: 주문 이력이 있는 고객 조회 문제

수도코드로 보기

이 고객에 대한 주문이 존재하는지 확인한다
주문이 하나라도 있으면 고객을 출력한다

 

SELECT *
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.id
);

 

실행 순서 (Line by Line)

customers 테이블의 각 행을 하나씩 확인
해당 customer_id로 orders에 행이 있는지 검사
하나라도 있으면 TRUE → 고객 출력

 


6. 상관 서브쿼리 — 바깥 쿼리를 참조하는 서브쿼리

지금까지 봤던 서브쿼리는 대부분 이러했다.

• 서브쿼리 혼자 실행 가능

• 한 번 실행 → 결과를 바깥 쿼리가 사용

허나, 상관 서브쿼리는 바깥 쿼리의 한 행마다 서브쿼리가 다시 실행된다. 

 

언제 사용하나?

“각 행마다 기준이 달라질 때”

  • 각 부서에서 평균보다 급여가 높은 직원
  • 각 고객의 주문 평균 금액보다 큰 주문
  • 각 상품의 카테고리 평균보다 비싼 상품

예시문제: 각 부서에서 자기 부서 평균 급여보다 많이 받는 직원 조회

수도코드로 보기

직원 한 명을 선택한다
이 직원이 속한 부서의 평균 급여를 계산한다
직원 급여가 그 평균보다 큰지 비교한다
크면 출력한다
다음 직원으로 이동한다

 

SELECT name, salary, department
FROM employees e
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
    WHERE department = e.department
);

 

실행 순서 (Line by Line)

1. employees 테이블에서 첫 번째 직원 e 선택
2. 서브쿼리 실행 → 이 직원의 부서 평균 급여 계산
3. e.salary > 부서 평균 급여 비교
4. 조건이 참이면 출력
5. 다음 직원으로 이동하여 반복

6. CTE (WITH) — 이름 붙인 서브쿼리

CTE는 FROM 서브쿼리에 이름을 붙인 형태다.
서브쿼리를 위에서 이름 붙여서 미리 정의한다. 때문에 읽기 쉽고, 재사용도 가능하다.

• FROM 절 서브쿼리가 길어질 때

• 같은 서브쿼리를 여러 번 써야 할 때

• 실행 순서를 명확히 보여주고 싶을 때

일때 사용한다.

예시문제: 부서별 평균 급여를 구한 뒤 평균 급여가 3000 이상인 부서 조회

수도코드로 보기

부서별 평균 급여 테이블을 만든다
이 테이블에 이름을 붙인다
그 결과에서 평균 급여가 3000 이상인 부서를 조회한다

 

WITH dept_avg AS (
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
)
SELECT department, avg_salary
FROM dept_avg
WHERE avg_salary >= 3000;

 

실행 순서 (Line by Line)

1. WITH 절 실행 → 부서별 평균 급여 테이블 생성 → dept_avg라는 이름으로 저장
2. 메인 SELECT 실행 FROM dept_avg
3. WHERE 조건 적용
4. 결과 출력

 


오늘 학습한 내용 정리

이번 챕터에서는 복잡한 SQL을 다루는 핵심 도구인 서브쿼리와 CTE를 복습했다.

  • 서브쿼리는 임시 계산 결과를 만드는 도구다
  • SELECT 절은 각 행 옆에 기준이 되는 값을 보여줄수 있다.
  • WHERE절은 조건에 쓸 기준값이 필요할 때, 조건을 비교해야 할 때 사용한다.
  • FROM절로 미리 가공한 결과를 임시테이블로 만들수 있다.
  • JOIN 서브쿼리로 테이블을 붙일수 있다.
  • EXISTS / NOT EXISTS 로 존재 여부를 파악할수 있다.
  • 상관 서브쿼리는 행 단위 로직에 사용된다
  • CTE는 가독성과 유지보수를 위한 구조다
 
 

1. JOIN을 하기 전에 먼저 생각해야 할 것

JOIN 문법을 작성하기 전에 반드시 정리해야 할 질문이 있다.

  • 각 테이블에서 한 행은 무엇을 의미하는가
  • PK와 FK는 무엇인가
  • 두 테이블은 어떤 관계인가 (1:1, 1:N)
  • 결과 행 수가 늘어나는 것이 의도한 것인가

이 질문에 답하지 않고 JOIN을 하면 쿼리는 실행되지만 결과가 잘못 나오는 경우가 매우 많다.


2. INNER JOIN — 공통으로 존재하는 데이터만 연결하기

 

개념 정리

INNER JOIN은  두 테이블의 조건 (ON)에 매칭되는 데이터만 남긴다.

→ 즉, 두 테이블에  조인 조건에 의해 공통된 것만 남는다. (교집합으로 이해하면 편하다.)

 

⭐어디까지 보는게 편하단 의미지 실제로 교집합은 아니다. PK/FK가 같은 행만 연결하는 관계 매칭이다

정확한 표현은 "ON 조건을 만족하는 행 쌍만 결과로 남는다"가 맞을것이다.

 

언제 사용하나?

“양쪽에 모두 존재하는 데이터만 보고 싶을 때”

- 실제로 수강신청이 있는 학생만 보고 싶을 때

- 주문 이력이 있는 고객만 보고 싶을 때

- 로그가 존재하는 유저만 분석 대상로 삼을 때

둘 다 있는 애들만 → INNER JOIN

예제 상황

  • users: 사용자 정보 테이블
  • orders: 주문 정보 테이블

각 주문은 반드시 한 명의 사용자와 연결되어 있다.

 

수도코드

users 테이블과 orders 테이블을 연결한다
user_id가 같은 행만 남긴다
사용자 이름과 주문 금액을 출력한다

 

실제 SQL 코드

SELECT u.user_id, u.name, o.order_id, o.total_amount 
FROM users u 
INNER JOIN orders o 
ON u.user_id = o.user_id;

 

Line by Line (실행 흐름)

1. FROM users 테이블을 기준으로 읽는다
2. JOIN orders 테이블을 ON 조건으로 연결한다
3. user_id가 일치하는 행만 남긴다
4. SELECT에 지정한 컬럼만 출력한다

INNER JOIN에서는 JOIN 조건을 만족하지 않는 행은 처음부터 제거된다.


3. LEFT JOIN — 기준 테이블은 모두 살린다

 

 

개념 정리

LEFT JOIN은 왼쪽 테이블의 행은 모두 유지하고, 오른쪽 테이블은 조건이 맞는 경우에만 붙인다.

매칭되지 않는 경우, 오른쪽 컬럼은 NULL이 된다.

→ “기준 테이블 + 참고 정보” 구조로 이해하는 것이 핵심이다.

 

언제 사용하나?

기준(모수)을 유지하고 싶을 때

- 전체 학생 중 수강신청 여부 확인

- 전체 고객 대비 주문 유무 분석

- 전체 상품 대비 판매 여부 확인

기준만 보고 싶다 → LEFT JOIN

 

수도코드

users 테이블을 기준으로 삼는다
orders 테이블을 user_id로 연결한다
주문이 없는 사용자도 결과에 포함한다

 

실제 SQL 코드

SELECT u.user_id, u.name, o.order_id 
FROM users u 
LEFT JOIN orders o 
ON u.user_id = o.user_id;

 

 

Line by Line (실행 흐름)

1. FROM users 테이블을 모두 읽는다
2. orders 테이블을 ON 조건으로 연결한다
3. 매칭되지 않는 경우 orders 컬럼은 NULL로 채운다
4. SELECT 컬럼을 출력한다.

⭐ LEFT JOIN의 핵심은 기준이 되는 테이블이 무엇인지 명확히 인식하는 것이다. ⭐


4. ON vs WHERE — LEFT JOIN에서 자주 발생하는 실수

LEFT JOIN을 사용했는데 조건을 잘못 작성해 결과가 INNER JOIN처럼 나오는 경우가 있다.

그 이유는 대부분 WHERE 조건 때문이다.

 

4.1) 예시로 확인하기

먼저 기억해야 할것은 ON 절은 JOIN 자체의 조건이다. 어떤 행을 서로 연결할 것인가? 오른쪽 테이블을 붙일지 말지를 결정정한다.

FROM A
LEFT JOIN B
ON A.id = B.id
AND B.status = 'DONE'

이 코드의 의미는 

A는 유지

B는...

• B의 id가 A의 id와 같고

status가 'DONE'인 경우

만약 조건에 맞지 않으면 B 컬럼은 NULL

FROM A
LEFT JOIN B
ON A.id = B.id
WHERE B.status = 'DONE'

JOIN은 일단 수행됨

그 뒤 B.status = 'DONE'인 행만 남김

INNER 조인을 쓴것과 차이를 보이지 않음

 

LEFT JOIN 결과에서 오른쪽 테이블이 매칭되지 않은 행은
B.status = NULL 이런 행일것이다. 이 상태에서
WHERE B.status = 'DONE' 를 사용하면?
NULL = 'DONE' → FALSE , 결국 해당 행은 제거됨
결과적으로  LEFT JOIN을 했지만, 결과는 INNER JOIN처럼 동작하게 된다.

 

이것만 기억하자.

ON은 “어떤 행끼리 붙일지” 정할 때 쓴다. (JOIN중 작동)

WHERE는 “붙인 결과 중 무엇을 보여줄지” 정할 때 쓴다. (JOIN 후 작동)

LEFT JOIN에서 오른쪽 테이블 조건은 ON에 쓰는 게 정석이다.

잘못된 예제

SELECT u.user_id, o.order_id 
FROM users u 
LEFT JOIN orders o 
ON u.user_id = o.user_id
WHERE o.status = 'completed';

이 경우, orders가 NULL인 행은 WHERE에서 제거된다.
결과적으로 LEFT JOIN의 의미가 사라진다.

올바른 방식

SELECT u.user_id, o.order_id
FROM users u 
LEFT JOIN orders o 
ON u.user_id = o.user_id 
	AND o.status = 'completed';

조건이 JOIN 대상에 대한 것이라면 WHERE가 아니라 ON에 작성해야 한다.


5. 다중 JOIN — 테이블이 3개 이상일 때

JOIN은 두 개만 할 수 있는 것이 아니다.
필요하다면 여러 테이블을 연속으로 연결할 수 있다.

  • A JOIN B → 결과 AB
  • (AB) JOIN C → 결과 ABC

중요한 건 “몇 개를 JOIN하느냐”가 아닌 JOIN을 한 번 할 때마다 결과가 어떻게 변하는지 설명할 수 있느냐다.

 

수도코드

users와 orders를 연결한다
orders와 payments를 연결한다
사용자별 주문과 결제 정보를 출력한다

 

실제 SQL 코드

SELECT u.user_id, o.order_id, p.payment_amount 
FROM users u 
LEFT JOIN orders o 
	ON u.user_id = o.user_id 
LEFT JOIN payments p 
	ON o.order_id = p.order_id;

 

 

실행 흐름

1. users 기준으로 orders를 LEFT JOIN
2. 그 결과를 기준으로 payments를 LEFT JOIN
3. SELECT 컬럼 출력

JOIN은 항상 앞 단계의 결과를 기반으로 다음 JOIN이 실행된다.


6. JOIN 결과 검증 — 반드시 확인해야 할 것

JOIN 결과를 확인할 때는 다음을 점검한다.

  • JOIN 전후 행 수가 왜 변했는가
  • 중복 행이 발생할 가능성은 없는가
  • 1:N 관계에서 집계가 필요한 상황은 아닌가

단순히 쿼리가 실행된다고 끝이 아니다.
결과가 의도한 데이터인지 검증하는 과정이 반드시 필요하다.


7. UNION vs JOIN — 언제 무엇을 써야 할까

JOIN

  • 테이블을 옆으로 붙인다
  • 컬럼 수가 늘어난다
  • 관계가 있는 데이터 연결에 사용

UNION

  • 결과를 위아래로 합친다
  • 컬럼 구조가 동일해야 한다
  • 서로 다른 조건의 결과를 합칠 때 사용

JOIN과 UNION은 목적이 완전히 다르다.
대체 관계가 아니라, 사용 상황이 다르다.


오늘 학습한 내용 정리

이번 챕터에서는 JOIN을 실전 관점에서 복습했다.

  • JOIN 전에 테이블의 의미와 관계를 먼저 정리해야 한다
  • 가장 많이 쓰는 JOIN은 INNER JOIN(교집합)과 LEFT JOIN(전체 테이블)이다
  • LEFT JOIN에서는 WHERE 조건 위치를 항상 주의해야 한다
  • 다중 JOIN은 순서와 기준 테이블이 중요하다
  • JOIN 결과는 반드시 행 수와 중복 여부를 검증해야 한다
  • JOIN과 UNION은 목적이 다르다
 

1. 데이터 딕셔너리 — 테이블을 읽는 설명서

데이터 딕셔너리는 단순한 컬럼 설명을 넘어서서 한 행(Row)이 무엇을 나타내는지와 테이블이 전체 데이터 모델에서 어떤 역할을 하는지 알려준다.

 

해당 개념은 저번 블로그 글에 작성되었기 때문에 참조.


2. PK (Primary Key)와 FK(Foreign Key) 

PK는 테이블에서 각 행을 유일하게 구분한다.
예를 들어 users.user_id는 각각 다른 사용자 한 명을 대표한다.
PK를 알고 있으면 JOIN 조건을 세울 때 기준이 된다.

 

 

FK는 다른 테이블의 PK를 참조하는 컬럼으로, JOIN이 이루어지는 기준이 된다.

orders.user_id는 users.user_id를 참조한다

 

이렇게 구조를 이해하면 JOIN문을 보기만 해도
무슨 일이 일어나는지 떠올릴 수 있다.


3. 관계 감각 (1:1 / 1:N) — cardinality 란?

관계 감각을 이해하는 것은 JOIN 결과를 예상하는 데 중요하다.

1:1 관계 — 한 행이 대응하는 한 행만 가진다
- 한 행 ↔ 한 행
- JOIN해도 행 수가 늘어나지 않음
- 결과 예측이 쉬움
1:N 관계 — 한 행이 여러 행과 대응한다
- 1쪽 테이블의 한 행
- N쪽 테이블의 여러 행과 매칭
- JOIN 순간 행이 N배로 늘어남
🚨대부분의 JOIN 사고는 여기서 발생

JOIN 결과에서 행 수가 갑자기 늘어나면 이 관계가 1:N이기 때문이다.

 

카디널리티를 케이크처럼 쉽게 먹는법

JOIN 전에 반드시 이 순서를 거친다.

  1. 최종 결과의 행 단위(grain)를 먼저 선언
    - 학생 단위인지?
    - 주문 단위인지?
    - 결제 단위인지?
  2. N쪽 테이블은
    - JOIN 전에 집계하거나
    - 필요한 정보만 추려서
    - 한 행으로 줄인다
  3. JOIN 후 결과를 반드시 확인
    - 행 수가 왜 늘었는지 설명 가능한가?

⭐중요⭐
JOIN은 붙이는 기술이 아니라, 결과를 설계하는 과정이다.
이 감각이 없다면 INNER / LEFT / RIGHT JOIN을 아무리 외워도 결과는 계속 틀린다.


4. UNION — 결과를 세로로 합치는 또 다른 개념

여기까지는 JOIN으로 테이블을 ‘가로로’ 연결하는 내용을 살펴봤다.
하지만 SQL에는 테이블을 ‘세로로 합치는 개념도 있다.
이것이 바로 UNION과 UNION ALL이다.

UNION과 UNION ALL의 기본 개념

UNION은 여러 SELECT 쿼리의 결과를 하나로 합친다. 이때 UNION은 중복된 행을 제거하고 결과를 반환한다.

 

UNION ALL은 여러 SELECT 결과중복 포함해서 모두 합친다. 즉, 행이 그대로 붙는다.

 

기억해야 할 규칙 4가지

  1. 각 SELECT는 같은 개수의 컬럼을 반환해야 한다.
    • 컬럼 수가 다르면 UNION 불가
  2. 같은 위치의 컬럼끼리 데이터 타입이 호환되어야 한다
    • (숫자 ↔ 숫자, 문자열 ↔ 문자열 등)
  3. 결과 컬럼명은 첫 번째 SELECT 기준
    • 컬럼명이 꼭 똑같아야 하나?
    X. 컬럼명은 달라도 되지만, UNION 결과의 컬럼명은 첫 번째 SELECT의 컬럼명(또는 별칭)을 따른다.
  4. UNION 결과 전체를 정렬하고 싶다면 맨 마지막에 ORDER BY를 사용한다.

UNION 기본 사용법

수도코드

학생 테이블에서 A 조건 학생 리스트
학생 테이블에서 B 조건 학생 리스트
두 리스트를 세로로 합친다 중복은 제거한다

UNION 예시

SELECT student_id, student_name 
FROM students 
WHERE grade = 'A' 
UNION 
SELECT student_id, student_name 
FROM students 
WHERE major = 'Data Analytics';

 

UNION ALL 예시

SELECT student_id, student_name 
FROM students 
WHERE grade = 'A' 
UNION ALL 
SELECT student_id, student_name 
FROM students 
WHERE major = 'Data Analytics';

이 경우 중복도 그대로 합쳐진다.

 

실행 흐름

  1. 첫 번째 SELECT 실행 → 결과 저장
  2. 두 번째 SELECT 실행 → 결과 저장
  3. 두 결과를 세로로 붙인다
  4. 중복이 있으면 제거한다 (UNION만 해당)

5. UNION과 정렬

UNION 결과에 정렬을 적용하려면
전체 결과에 대해 ORDER BY를 마지막에 한 번만 지정할 수 있다.

SELECT student_id, student_name 
FROM a 
UNION 
SELECT student_id, student_name 
FROM b
ORDER BY student_name;

오늘 학습한 내용 정리

이번 챕터에서는 테이블 구조를 이해하는 기본 개념과 JOIN을 하기 전에 점검해야 할 요소들을 복습했다.

  • 데이터 딕셔너리는 테이블의 의미를 설명한다 (링크 참조)
  • PK와 FK는 JOIN 조건을 이해하는 기준이다
  • 관계(1:1, 1:N)는 결과 행 수를 예상하게 한다
  • UNION은 세로 결합 개념이다
  • UNION은 중복 제거, UNION ALL은 중복 포함 결과를 반환한다

SQL을 공부하면서 많은 사람이 문법을 먼저 외운다.
하지만 실제로 잘 작동하는 SQL을 작성하려면 문법보다 먼저 데이터 구조 자체를 이해하는 능력이 필요하다.

 

이때 가장 중요한 개념이 데이터 딕셔너리(Data Dictionary)라 할수 있다.
데이터 딕셔너리는 단순히 “컬럼 이름과 설명 목록”이 아닌,
테이블의 역할, 한 행이 무엇을 의미하는지, 그리고 다른 테이블과의 관계까지를 정의한 설계 문서에 가깝다.


데이터 딕셔너리가 필요한 이유

SQL은 단순한 조회 문법이 아니다. 데이터를 조합하고 해석해서 의미 있는 결과를 만드는 도구에 가깝다.
그런데 쿼리를 작성할 때 가장 먼저 해야 할 질문은 다음과 같다.

“이 테이블에서 한 행(Row)은 무엇을 의미하는가?”

이 질문에 답하지 않은 채로 추후에 공부하게될 JOIN같은 문법을 사용하면,
결과가 의도와 다르게 늘어나거나, 줄어들거나, 해석이 불가능해지는 경우가 많다.
이것이 바로 데이터 딕셔너리가 필요한 이유다.


데이터 딕셔너리에 담겨야 할 핵심 요소

데이터 딕셔너리를 작성할 때는 다음 항목들을 중심으로 정리하는 것이 좋다.

1. 테이블 요약

먼저 테이블이 어떤 용도의 테이블인지 정의한다.

예)

  • students — 학생 마스터 테이블
    한 행 = 한 학생 정보

테이블 요약은 “한 행의 의미”를 정확히 이해하기 위한 출발점이다.


2. 테이블 설명

이 테이블이 어떤 성격인지 구체화한다.

  • 이벤트성 테이블인가?
  • 마스터 테이블인가?
  • 시간이 지나도 변하지 않는 정보인가?

이 질문에 답할 수 있으면 쿼리 작성 시 의도와 결과 사이의 괴리를 줄일 수 있다.


3. PK (Primary Key)

테이블에서 행을 고유하게 식별하는 컬럼을 정의한다.

예)

  • users 테이블의 user_id
  • orders 테이블의 order_id

Primary Key는 마치 그 테이블의 “주민등록번호”와 같다. 


4. 주요 JOIN 키

다른 테이블과 어떻게 연결되는지를 적는다.

이때 중요한 것은 다음 두 가지다.

  • 어떤 컬럼이 FK(외래키)로서 JOIN 키인지
  • 이 관계가 1:1인지, 1:N인지

이 정보는 JOIN 결과에서 행 수가 늘어나는 이유, 줄어드는 이유를 판단하는 데 필수적이다.


5. 컬럼 정의

각 컬럼이 어떤 의미를 갖는지, 값의 범위는 어떤지 등을 정리한다.

많은 사람들이 이 단계만 보고 데이터 딕셔너리라고 생각하지만,
사실 이 부분은 전체 딕셔너리에서 일부에 불과하다.


데이터 딕셔너리가 SQL 작성에 미치는 영향

SQL을 단순히 문법으로 이해하는 것은 한계가 있다.
하지만 데이터 딕셔너리를 먼저 정리하면 다음과 같은 이점이 생긴다.

  • 쿼리의 결과 한 행이 무엇을 의미하는지 명확해진다.
  • JOIN을 하기 전에 집계가 먼저 필요한지 판단할 수 있다.
  • JOIN 결과를 보고 정상/비정상을 바로 구분할 수 있다.

즉, SQL은 “쿼리를 잘 쓰는 기술”이 아니라, “데이터 구조를 이해하고 그 결과를 설계하는 기술”이라고 할 수 있다.


데이터 딕셔너리는 어떻게 만들까? (실습 관점)

-이런 느낌이구나 하고 짚고 넘어가기-

예시 테이블 — users

테이블 이름: users
한 행의 의미: 가입한 한 명의 사용자 정보
PK: user_id
주요 컬럼: user_id, name, email, created_at 

이렇게 정리하면
쿼리를 작성할 때 “users 테이블에서 한 사람이 어떤 의미인가”가 곧바로 떠오른다.

 

예시 테이블 — orders 

테이블 이름: orders
한 행의 의미: 한 건의 주문 정보
PK: order_id FK: user_id
주요 컬럼: order_id, user_id, total_amount, status, created_at
JOIN 관계: users.user_id = orders.user_id

이렇게 정리하면

users와 orders를 JOIN할 때 “각 주문이 어떤 사용자와 연결되는지”가 곧바로 보인다.


오늘 학습한 내용 정리

이번 글에서는 데이터 딕셔너리의 필요성과 그 구조를 어떻게 정리해야 하는지를 복습했다.

  • 데이터 딕셔너리는 단순 컬럼 설명 목록이 아니다
  • 한 행의 의미(Grain)를 먼저 정의해야 한다
  • PK와 JOIN 키를 알면 JOIN 결과를 해석할 수 있다
  • 딕셔너리가 SQL 작성의 설계도 역할을 한다

데이터 딕셔너리를 먼저 작성하는 습관은
SQL 쿼리를 정확하게 해석하고 안정적으로 작성하는 능력으로 이어진다.

1. 집계란 무엇인가

집계는 여러 행을 하나의 숫자로 요약하는 작업이다.

예를 들어 다음과 같은 질문은 모두 집계를 필요로 한다.

  • 전체 주문 건수는 몇 개인가
  • 총 매출은 얼마인가
  • 평균 점수는 얼마인가

이 질문들의 공통점은 개별 행이 아니라, 여러 행을 하나의 값으로 요약한다는 점이다.

SQL에서는 이를 집계 함수로 처리한다.


2. 기본 집계 함수 살펴보기

자주 사용하는 집계 함수는 다음과 같다.

• COUNT: 조건을 만족하는 행의 개수를 센다.

• SUM: 숫자형 컬럼의 총합

• AVG: 숫자형 컬럼의 평균값

• MIN: 가장 작은 값을 반환

• MAX: 가장 큰 값을 반환

SELECT
        COUNT(unit_price) AS item_rows,
        SUM(unit_price) AS item_price_sum,
        AVG(unit_price) AS item_price_avg,
        MIN(unit_price) AS item_price_min,
        MAX(unit_price) AS item_price_max
FROM basic.order_items;

#일반적으로 집계 함수는 NULL을 무시한다. 일반적으로 집계 함수는 NULL 값을 자동으로 제외하고 계산
• SUM, AVG, MIN, MAX, COUNT(컬럼) → NULL 무시
• COUNT(*) → NULL 포함 (행 단위 계산)

이 차이 때문에, COUNT 계열은 특히 주의해서 사용해야 한다.

 

2.1) COUNT 사용법 3종

COUNT(*)

NULL 여부와 상관없이  결과 행(row) 자체의 개수를 카운트


COUNT(컬럼)

해당 컬럼이 NULL이 아닌 행만 카운트


COUNT(DISTINCT 컬럼)

- NULL값 제외한을 제외한 서로 다른 값의 개수를 센다. (=고유값)

[DISTINCT는 중복된 값을 제거하고 유일한 값만 남긴다.]

 

예제: 전체 주문 건수 구하기

수도코드

orders 테이블에 있는 모든 주문의 개수를 센다

 

실제 SQL 코드

SELECT COUNT(*) AS order_count 
FROM orders;

실행 흐름 (Line by Line)

1. orders 테이블의 모든 행을 가져온다.
2. 가져온 행 전체를 대상으로 개수를 계산한다.
3. 결과는 하나의 행, 하나의 컬럼으로 반환된다.

이처럼 집계 함수는 여러 행을 하나의 결과로 줄이는 연산이다.


3. GROUP BY () — 기준을 나누어 집계하기

전체 합계만으로는 부족한 경우가 많다. 대부분의 분석에서는 “기준별 요약”이 필요하다.

GROUP BY는 집계의 기준을 나누는 역할을 한다.

즉, 여러 행을 특정 기준으로 묶어서 그룹 단위로 집계 결과를 만들기 위해 사용한다.

 

3.1) GROUP BY ()를 사용하는 이유?

GROUP BY()를 사용하면 모든 행에 대해 값을 집계해 계산하기 때문에 행은 1개로 줄어든다.

허나 실제로 사용자들이 원하는 것은

• 전체 매출이 아닌 카테고리별 매출

• 전체 인원이 아닌 부서별 인원의 수

• 전체 주문이 아닌 사용자별 주문 횟수

대부분의 상황에선 전체가 아닌, 특정 기준별로 나눠서 보고 싶어 한다.

이런 상황에서의 해결책이 바로 GROUP BY 이다.

 

3.2) GROUP BY ()의 역할

• 해당 컬럼(괄호안의 컬럼)의 값이 같은 행들을 묶여 그룹이 된다. (Ex. 주문번호, 결제번호 등등)

각 묶음마다 집계 함수를 계산한다

결과는 그룹의 개수만큼 행이 나온다.

 

3.3) SELECT절과 GROUP BY와의 관계

만약 GROUP BY를 사용했을 경우 SELECT 절에는 다음과 같은 컬럼만 올수 있다,

GROUP BY에 사용한 컬럼

집계 함수로 감싼 컬럼

그 이유는.

그룹 안에 여러행이 있는데 그중에 어떠한 값을 보여줘야 할지 SQL은 알수 없기 때문이다.

- 어렵다면 GROUP BY한 컬럼과 SELECT절에서 출력한 컬럼이 매치가 안되면 오류 발생

예제: 사용자별 주문 건수 구하기

수도코드

사용자별로 주문을 묶는다
각 사용자 그룹마다 주문 개수를 센다

 

실제 SQL 코드

SELECT
	user_id, 
    	COUNT(*) AS order_count 
FROM orders 
GROUP BY user_id;

실행 흐름 (Line by Line)

1. orders 테이블의 모든 행을 가져온다.
2. user_id 기준으로 행을 그룹으로 나눈다.
3. 각 그룹마다 COUNT(*)를 계산한다.
4. 결과는 사용자 수만큼의 행으로 반환된다.

GROUP BY를 사용하는 순간부터 집계는 그룹 단위로 계산된다는 점이 중요하다.


4. HAVING — 집계 결과를 필터링하는 조건

HAVING은 집계가 끝난 결과를 대상으로 조건을 거는 절로 GROUP BY로 만들어진 그룹 단위의 결과를 걸러내기 위해 사용한다.

SQL에서 COUNT, SUM, AVG 같은 집계 함수는 개별 행이 아니라 그룹 전체를 계산한 뒤에야 값이 만들어진다.
HAVING은 바로 이 집계 결과가 만들어진 이후 단계에서 동작한다.

예제 주문이 3건 이상인 사용자만 보기

수도코드

사용자별로 주문 개수를 센다
주문 개수가 3 이상인 사용자만 남긴다
 
실제 SQL 코드
SELECT 
	user_id, 
    	COUNT(*) AS order_count 
FROM orders 
GROUP BY user_id 
HAVING COUNT(*) >= 3;

 

실행 흐름 (Line by Line)

1. 전체 주문 데이터를 가져온다.
2. 사용자별로 그룹을 만든다.
3. 각 그룹의 주문 개수를 계산한다.
4. 계산된 개수가 3 이상인 그룹만 남긴다.

 

WHERE와 HAVING의 차이

집계를 하다 보면 조건을 어디에 써야 할지 헷갈리는 경우가 많다.

핵심 차이는 다음과 같다.

  • WHERE: 집계 전에 개별 행(row) 을 필터링한다
  • HAVING: 집계 후에 그룹(group) 을 필터링한다

5. 조건부 집계 

실무에서 매우 자주 사용되는 패턴이 조건부 집계다.

GROUP BY된 그룹 내부에서 특정 조건을 만족하는 행만 대상으로 집계하는 방법이다.

 

SUM(CASE WHEN) 패턴

“조건을 만족한 대상의 총합”을 구할 때 사용한다. COUNT와 달리 ELSE 0이 매우 중요 (NULL이면 합산 누락)

SUM(CASE WHEN 조건 THEN 값 ELSE 0 END)

CASE로 조건을 만족하면 값 반환, 조건을 만족하지 않으면 0 반환한다.

SUM 결과는 조건을 만족한 값만 누적된 합계이다.

 

COUNT(CASE WHEN) 패턴

“조건을 만족한 행의 개수”를 세고 싶을 때 사용한다. TRUE / FALSE를 세는 것이 아닌 NULL 여부를 이용해 필터링

COUNT(CASE WHEN 조건 THEN PK END)

CASE로 조건을 만족하면 NULL이 아닌 값, 조건을 만족하지 않으면 NULL을 반환한다.

COUNT는 NULL을 세지 않기 때문에 결과적으로 조건을 만족한 행만 카운트하게 된다.

 

SUM  vs COUNT 차이 정리

조건부 SUM 조건부 COUNT
얼마인가? (규모, 금액, 수치) 몇 건인가? (개수)
조건 불만족 시 0으로 누적 차단 NULL 여부를 이용한 필터링

 

예제: 상태별 주문 건수 계산하기

주문 상태가 다음과 같다고 가정한다.

  • status = 'PAID'
  • status = 'CANCELLED'

수도코드

주문 상태가 PAID인 경우만 1로 센다
주문 상태가 CANCELLED인 경우만 1로 센다.

 

실제 SQL 코드

SELECT 
		SUM(CASE 
        		WHEN status = 'PAID' THEN 1 
            		ELSE 0 
        	END) AS paid_count, 
        	SUM(CASE 
            		WHEN status = 'CANCELLED' THEN 1 
                	ELSE 0 
            	END) AS cancelled_count 
FROM orders;

 

실행 흐름 (Line by Line)

1. 모든 주문 행을 가져온다
2. 각 행마다 조건에 따라 1 또는 0을 반환한다

3. 반환된 숫자들을 모두 더한다..

오늘 학습한 내용 정리

이번 글에서는 데이터를 숫자로 요약하는 집계와 그룹화 그리고 조건부 집계에 대해 복습했다.

  • 집계는 여러 행을 하나의 값으로 줄이는 연산이다. (COUNT, SUM, AVG, MIN, MAX)
  • GROUP BY는 집계를 위한 기준을 만들어 행을 그룹 단위로 묶는다.
  • WHERE는 집계 이후에 그룹 결과를 필터링한다.
  • SUM, COUNT를 사용해 조건부 집계를 할수 있다.

+ Recent posts