Skip to Content
이론 및 개념 (Concepts)SQLJOIN을 활용한 테이블 결합

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()
ℹ️
프로젝트 ID 설정

위 코드에서 your-project-id를 본인의 GCP 프로젝트 ID로 변경하세요.


1. JOIN이란?

이론

JOIN은 두 개 이상의 테이블을 특정 조건으로 결합하는 SQL 연산입니다. 관계형 데이터베이스에서 데이터는 여러 테이블에 분산 저장되므로, 의미있는 분석을 위해 테이블을 결합해야 합니다.

집합 이론으로 이해하기

JOIN 유형집합 연산설명
INNER JOINA ∩ B두 테이블에 모두 존재하는 행만
LEFT JOINA + (A ∩ B)왼쪽 테이블 전체 + 매칭되는 오른쪽
RIGHT JOIN(A ∩ B) + B오른쪽 테이블 전체 + 매칭되는 왼쪽
FULL OUTER JOINA ∪ B양쪽 테이블 모두
CROSS JOINA × B모든 가능한 조합 (곱집합)

기본 구문

SELECT columns FROM table_a [INNER | LEFT | RIGHT | FULL OUTER | CROSS] JOIN table_b ON table_a.key = table_b.key

2. 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_iduser_idstatuscreated_atproduct_idsale_price
154321Complete2024-01-15 10:30:001234559.99
154321Complete2024-01-15 10:30:006789029.99
212345Complete2024-01-15 11:45:001111189.99
💡
테이블 별칭 (Alias)

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_idfirst_namelast_nameemailorder_counttotal_spent
59071JohnSmithjohn.smith@example.com81633.89
84005JaneDoejane.doe@example.com41500.73
90572BobJohnsonbob.j@example.com61477.39
12345AliceBrownalice.b@example.com00
ℹ️
LEFT JOIN 활용
  • 주문하지 않은 고객 찾기 (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_idfirst_namelast_nameemailsignup_date
99876MikeWilsonmike.w@example.com2024-11-15
99543SarahLeesarah.l@example.com2024-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 10

5. 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_namecategorycampaign_name
Black FridayAccessoriesBlack Friday - Accessories
Black FridayActiveBlack Friday - Active
Black FridayJeansBlack Friday - Jeans
Summer SaleAccessoriesSummer 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_idorder_datestatuscustomer_namecountryproduct_namecategorybrandsale_priceprofit
1250002024-11-15CompleteJohn SmithUnited StatesClassic JeansJeansLevi’s89.9935.50
1249992024-11-15ShippedJane DoeChinaCotton T-ShirtTops & TeesH&M24.9912.30

7. JOIN 성능 최적화

핵심 원칙

  1. JOIN 키에 인덱스 사용: BigQuery는 자동 최적화하지만, 파티션/클러스터링 키 활용
  2. 작은 테이블을 오른쪽에: BigQuery는 오른쪽 테이블을 브로드캐스트
  3. 필요한 컬럼만 SELECT: SELECT * 지양
  4. 조기 필터링: 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_id

EXPLAIN으로 실행 계획 확인

-- 쿼리 앞에 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

실행 결과:

categorybrandtotal_revenueitems_soldrank
AccessoriesRay-Ban125,340.501,5231
AccessoriesOakley98,230.751,2452
AccessoriesPersol87,650.009873
JeansLevi’s234,560.252,8901

문제 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_idfirst_namelast_namefirst_order_datelast_order_datetotal_ordersdays_as_customer
4523MichaelChen2023-01-022024-11-1512682
7891EmilyWang2023-02-152024-10-289621

문제 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_idnamecategorybrandretail_priceproduct_created_at
28456Premium Jacket XOuterwear & CoatsCanada Goose899.992024-10-01
29012Designer Dress YDressesGucci750.002024-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와 집계 함수를 배워보세요.

Last updated on

🤖AI 모의면접실전처럼 연습하기