JOIN을 활용한 테이블 결합
학습 목표
이 레시피를 완료하면 다음을 할 수 있습니다:
- JOIN의 개념과 집합 이론 이해
- INNER JOIN으로 교집합 데이터 추출
- LEFT/RIGHT JOIN으로 기준 테이블 유지
- FULL OUTER JOIN으로 합집합 생성
- CROSS JOIN으로 모든 조합 생성
- 다중 테이블 JOIN 및 성능 최적화
BigQuery 실행 환경 설정
from google.cloud import bigquery
import pandas as pd
# 인증 설정 (서비스 계정 키 사용 시)
# import os
# os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "path/to/your-key.json"
# BigQuery 클라이언트 생성
client = bigquery.Client(project='your-project-id')
# 쿼리 실행 함수
def run_query(query):
return client.query(query).to_dataframe()위 코드에서 your-project-id를 본인의 GCP 프로젝트 ID로 변경하세요.
1. JOIN이란?
이론
JOIN은 두 개 이상의 테이블을 특정 조건으로 결합하는 SQL 연산입니다. 관계형 데이터베이스에서 데이터는 여러 테이블에 분산 저장되므로, 의미있는 분석을 위해 테이블을 결합해야 합니다.
집합 이론으로 이해하기
| JOIN 유형 | 집합 연산 | 설명 |
|---|---|---|
| INNER JOIN | A ∩ B | 두 테이블에 모두 존재하는 행만 |
| LEFT JOIN | A + (A ∩ B) | 왼쪽 테이블 전체 + 매칭되는 오른쪽 |
| RIGHT JOIN | (A ∩ B) + B | 오른쪽 테이블 전체 + 매칭되는 왼쪽 |
| FULL OUTER JOIN | A ∪ B | 양쪽 테이블 모두 |
| CROSS JOIN | A × B | 모든 가능한 조합 (곱집합) |
기본 구문
SELECT columns
FROM table_a
[INNER | LEFT | RIGHT | FULL OUTER | CROSS] JOIN table_b
ON table_a.key = table_b.key2. INNER JOIN - 교집합
이론
INNER JOIN은 양쪽 테이블에서 조건이 일치하는 행만 반환합니다. 가장 많이 사용되는 JOIN 유형입니다.
테이블 A 테이블 B INNER JOIN 결과
+----+----+ +----+----+ +----+----+----+
| id | a | | id | b | | id | a | b |
+----+----+ +----+----+ +----+----+----+
| 1 | X | | 1 | P | → | 1 | X | P |
| 2 | Y | | 3 | Q | | 3 | Z | Q |
| 3 | Z | | 4 | R | +----+----+----+
+----+----+ +----+----+실습: 주문과 주문 상세 결합
SELECT
o.order_id,
o.user_id,
o.status,
o.created_at,
oi.product_id,
oi.sale_price
FROM `your-project-id.thelook_ecommerce.orders` o
INNER JOIN `your-project-id.thelook_ecommerce.order_items` oi
ON o.order_id = oi.order_id
WHERE o.status = 'Complete'
LIMIT 10실행 결과:
| order_id | user_id | status | created_at | product_id | sale_price |
|---|---|---|---|---|---|
| 1 | 54321 | Complete | 2024-01-15 10:30:00 | 12345 | 59.99 |
| 1 | 54321 | Complete | 2024-01-15 10:30:00 | 67890 | 29.99 |
| 2 | 12345 | Complete | 2024-01-15 11:45:00 | 11111 | 89.99 |
orders o처럼 테이블에 별칭을 부여하면 쿼리가 간결해집니다. 특히 여러 테이블을 JOIN할 때 필수입니다.
3. LEFT JOIN - 왼쪽 기준
이론
LEFT JOIN은 왼쪽 테이블의 모든 행을 유지하고, 오른쪽 테이블에서 매칭되는 행을 결합합니다. 매칭되지 않으면 NULL로 채워집니다.
테이블 A 테이블 B LEFT JOIN 결과
+----+----+ +----+----+ +----+----+------+
| id | a | | id | b | | id | a | b |
+----+----+ +----+----+ +----+----+------+
| 1 | X | | 1 | P | → | 1 | X | P |
| 2 | Y | | 3 | Q | | 2 | Y | NULL |
| 3 | Z | | 4 | R | | 3 | Z | Q |
+----+----+ +----+----+ +----+----+------+실습: 모든 고객과 주문 이력
SELECT
u.id AS user_id,
u.first_name,
u.last_name,
u.email,
COUNT(o.order_id) AS order_count,
COALESCE(SUM(oi.sale_price), 0) AS total_spent
FROM `your-project-id.thelook_ecommerce.users` u
LEFT JOIN `your-project-id.thelook_ecommerce.orders` o
ON u.id = o.user_id
LEFT JOIN `your-project-id.thelook_ecommerce.order_items` oi
ON o.order_id = oi.order_id
GROUP BY u.id, u.first_name, u.last_name, u.email
ORDER BY total_spent DESC
LIMIT 15실행 결과:
| user_id | first_name | last_name | order_count | total_spent | |
|---|---|---|---|---|---|
| 59071 | John | Smith | john.smith@example.com | 8 | 1633.89 |
| 84005 | Jane | Doe | jane.doe@example.com | 4 | 1500.73 |
| 90572 | Bob | Johnson | bob.j@example.com | 6 | 1477.39 |
| 12345 | Alice | Brown | alice.b@example.com | 0 | 0 |
- 주문하지 않은 고객 찾기 (
WHERE o.order_id IS NULL) - 리뷰 없는 제품 찾기
- 결제 실패한 주문 찾기
실습: 주문하지 않은 고객 찾기
SELECT
u.id AS user_id,
u.first_name,
u.last_name,
u.email,
u.created_at AS signup_date
FROM `your-project-id.thelook_ecommerce.users` u
LEFT JOIN `your-project-id.thelook_ecommerce.orders` o
ON u.id = o.user_id
WHERE o.order_id IS NULL
ORDER BY u.created_at DESC
LIMIT 10실행 결과:
| user_id | first_name | last_name | signup_date | |
|---|---|---|---|---|
| 99876 | Mike | Wilson | mike.w@example.com | 2024-11-15 |
| 99543 | Sarah | Lee | sarah.l@example.com | 2024-11-14 |
4. RIGHT JOIN과 FULL OUTER JOIN
RIGHT JOIN
RIGHT JOIN은 LEFT JOIN의 반대입니다. 오른쪽 테이블의 모든 행을 유지합니다.
-- RIGHT JOIN (실무에서는 LEFT JOIN을 더 많이 사용)
SELECT
o.order_id,
o.status,
u.first_name,
u.last_name
FROM `your-project-id.thelook_ecommerce.users` u
RIGHT JOIN `your-project-id.thelook_ecommerce.orders` o
ON u.id = o.user_id
LIMIT 10대부분의 경우 RIGHT JOIN 대신 테이블 순서를 바꿔 LEFT JOIN을 사용합니다. 가독성이 더 좋습니다.
FULL OUTER JOIN
FULL OUTER JOIN은 양쪽 테이블의 모든 행을 포함합니다.
-- 모든 사용자와 모든 주문 (매칭 여부 상관없이)
SELECT
u.id AS user_id,
u.first_name,
o.order_id,
o.status
FROM `your-project-id.thelook_ecommerce.users` u
FULL OUTER JOIN `your-project-id.thelook_ecommerce.orders` o
ON u.id = o.user_id
WHERE u.id IS NULL OR o.order_id IS NULL -- 매칭 안 된 것만
LIMIT 105. CROSS JOIN - 곱집합
이론
CROSS JOIN은 두 테이블의 모든 가능한 조합을 생성합니다. 조건 없이 결합하므로 결과 행 수 = A 행 수 × B 행 수입니다.
실습: 모든 제품-프로모션 조합 생성
-- 프로모션과 카테고리의 모든 조합
WITH promotions AS (
SELECT promotion_name FROM UNNEST(['Black Friday', 'Summer Sale', 'New Year']) AS promotion_name
),
categories AS (
SELECT DISTINCT category
FROM `your-project-id.thelook_ecommerce.products`
LIMIT 5
)
SELECT
p.promotion_name,
c.category,
CONCAT(p.promotion_name, ' - ', c.category) AS campaign_name
FROM promotions p
CROSS JOIN categories c
ORDER BY p.promotion_name, c.category실행 결과:
| promotion_name | category | campaign_name |
|---|---|---|
| Black Friday | Accessories | Black Friday - Accessories |
| Black Friday | Active | Black Friday - Active |
| Black Friday | Jeans | Black Friday - Jeans |
| Summer Sale | Accessories | Summer Sale - Accessories |
| … | … | … |
CROSS JOIN은 결과가 급격히 커질 수 있습니다. 1만 행 × 1만 행 = 1억 행! 신중하게 사용하세요.
6. 다중 테이블 JOIN
실습: 주문 상세 분석 (4개 테이블)
SELECT
o.order_id,
o.created_at AS order_date,
o.status,
u.first_name || ' ' || u.last_name AS customer_name,
u.country,
p.name AS product_name,
p.category,
p.brand,
oi.sale_price,
p.retail_price,
ROUND(oi.sale_price - p.cost, 2) AS profit
FROM `your-project-id.thelook_ecommerce.orders` o
INNER JOIN `your-project-id.thelook_ecommerce.users` u
ON o.user_id = u.id
INNER JOIN `your-project-id.thelook_ecommerce.order_items` oi
ON o.order_id = oi.order_id
INNER JOIN `your-project-id.thelook_ecommerce.products` p
ON oi.product_id = p.id
WHERE o.created_at >= '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 15실행 결과:
| order_id | order_date | status | customer_name | country | product_name | category | brand | sale_price | profit |
|---|---|---|---|---|---|---|---|---|---|
| 125000 | 2024-11-15 | Complete | John Smith | United States | Classic Jeans | Jeans | Levi’s | 89.99 | 35.50 |
| 124999 | 2024-11-15 | Shipped | Jane Doe | China | Cotton T-Shirt | Tops & Tees | H&M | 24.99 | 12.30 |
7. JOIN 성능 최적화
핵심 원칙
- JOIN 키에 인덱스 사용: BigQuery는 자동 최적화하지만, 파티션/클러스터링 키 활용
- 작은 테이블을 오른쪽에: BigQuery는 오른쪽 테이블을 브로드캐스트
- 필요한 컬럼만 SELECT:
SELECT *지양 - 조기 필터링: JOIN 전에 WHERE로 데이터 축소
좋은 예 vs 나쁜 예
-- ❌ 나쁜 예: JOIN 후 필터링
SELECT *
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.created_at >= '2024-01-01'
-- ✅ 좋은 예: 서브쿼리로 먼저 필터링
SELECT o.order_id, o.status, oi.sale_price
FROM (
SELECT order_id, status
FROM orders
WHERE created_at >= '2024-01-01'
) o
JOIN order_items oi ON o.order_id = oi.order_idEXPLAIN으로 실행 계획 확인
-- 쿼리 앞에 EXPLAIN 추가
EXPLAIN
SELECT o.order_id, COUNT(oi.id)
FROM `your-project-id.thelook_ecommerce.orders` o
JOIN `your-project-id.thelook_ecommerce.order_items` oi
ON o.order_id = oi.order_id
GROUP BY o.order_id퀴즈
문제 1: 카테고리별 매출 Top 3 브랜드
각 카테고리에서 매출 상위 3개 브랜드를 조회하세요.
힌트: products, order_items JOIN 후 ROW_NUMBER() 사용
정답 보기
WITH brand_revenue AS (
SELECT
p.category,
p.brand,
SUM(oi.sale_price) AS total_revenue,
COUNT(*) AS items_sold
FROM `your-project-id.thelook_ecommerce.order_items` oi
INNER JOIN `your-project-id.thelook_ecommerce.products` p
ON oi.product_id = p.id
GROUP BY p.category, p.brand
),
ranked AS (
SELECT
category,
brand,
total_revenue,
items_sold,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_revenue DESC) AS rank
FROM brand_revenue
)
SELECT
category,
brand,
total_revenue,
items_sold,
rank
FROM ranked
WHERE rank <= 3
ORDER BY category, rank실행 결과:
| category | brand | total_revenue | items_sold | rank |
|---|---|---|---|---|
| Accessories | Ray-Ban | 125,340.50 | 1,523 | 1 |
| Accessories | Oakley | 98,230.75 | 1,245 | 2 |
| Accessories | Persol | 87,650.00 | 987 | 3 |
| Jeans | Levi’s | 234,560.25 | 2,890 | 1 |
| … | … | … | … | … |
문제 2: 재구매 고객 분석
2회 이상 주문한 고객의 첫 주문과 마지막 주문 정보를 조회하세요.
힌트: orders 테이블을 자기 자신과 JOIN (Self Join)
정답 보기
WITH customer_orders AS (
SELECT
user_id,
MIN(created_at) AS first_order_date,
MAX(created_at) AS last_order_date,
COUNT(DISTINCT order_id) AS total_orders
FROM `your-project-id.thelook_ecommerce.orders`
WHERE status = 'Complete'
GROUP BY user_id
HAVING COUNT(DISTINCT order_id) >= 2
)
SELECT
co.user_id,
u.first_name,
u.last_name,
co.first_order_date,
co.last_order_date,
co.total_orders,
DATE_DIFF(CAST(co.last_order_date AS DATE), CAST(co.first_order_date AS DATE), DAY) AS days_as_customer
FROM customer_orders co
INNER JOIN `your-project-id.thelook_ecommerce.users` u
ON co.user_id = u.id
ORDER BY co.total_orders DESC, days_as_customer DESC
LIMIT 20실행 결과:
| user_id | first_name | last_name | first_order_date | last_order_date | total_orders | days_as_customer |
|---|---|---|---|---|---|---|
| 4523 | Michael | Chen | 2023-01-02 | 2024-11-15 | 12 | 682 |
| 7891 | Emily | Wang | 2023-02-15 | 2024-10-28 | 9 | 621 |
문제 3: 판매되지 않은 제품 찾기
한 번도 판매되지 않은 제품을 찾으세요.
힌트: LEFT JOIN + IS NULL
정답 보기
SELECT
p.id AS product_id,
p.name,
p.category,
p.brand,
p.retail_price,
p.created_at AS product_created_at
FROM `your-project-id.thelook_ecommerce.products` p
LEFT JOIN `your-project-id.thelook_ecommerce.order_items` oi
ON p.id = oi.product_id
WHERE oi.id IS NULL
ORDER BY p.retail_price DESC
LIMIT 20실행 결과:
| product_id | name | category | brand | retail_price | product_created_at |
|---|---|---|---|---|---|
| 28456 | Premium Jacket X | Outerwear & Coats | Canada Goose | 899.99 | 2024-10-01 |
| 29012 | Designer Dress Y | Dresses | Gucci | 750.00 | 2024-09-15 |
비즈니스 인사이트:
- 고가 제품이 많다면 가격 정책 재검토
- 신규 제품이라면 마케팅 강화
- 재고 정리 대상 식별
정리
| JOIN 유형 | 사용 시점 | 결과 |
|---|---|---|
INNER JOIN | 양쪽 모두 있는 데이터만 필요 | 교집합 |
LEFT JOIN | 왼쪽 전체 + 매칭되는 오른쪽 | 왼쪽 기준 |
RIGHT JOIN | 오른쪽 전체 + 매칭되는 왼쪽 | 오른쪽 기준 |
FULL OUTER JOIN | 양쪽 모두 포함 | 합집합 |
CROSS JOIN | 모든 조합 생성 | 곱집합 |
JOIN 선택 가이드
필요한 데이터가?
├─ 양쪽 모두 있어야 함 → INNER JOIN
├─ 한쪽은 전부, 다른 쪽은 있으면 → LEFT/RIGHT JOIN
├─ 양쪽 전부 다 필요 → FULL OUTER JOIN
└─ 모든 조합 필요 → CROSS JOIN다음 단계
JOIN을 마스터했습니다! 다음으로 집계와 그룹화에서 GROUP BY와 집계 함수를 배워보세요.