Type something to search...

12 SQL LEFT JOIN 실습

1. 인트로

INNER JOIN이 “두 테이블 모두에 일치하는 데이터”만 가져온다면, LEFT JOIN(레프트 조인)은 왼쪽 테이블의 모든 데이터를 가져온다. 오른쪽 테이블에 일치하는 값이 없으면 NULL(널)로 채운다. “주문하지 않은 회원도 포함하여 전체 회원 목록을 보여줘”와 같은 상황에서 필수이다.


2. 실습 DB 준비

1
import sqlite3
2
import pandas as pd
3
4
conn = sqlite3.connect("cafe.db")
5
cur = conn.cursor()
6
7
# 메뉴 테이블
8
cur.execute("""
9
CREATE TABLE IF NOT EXISTS menus (
10
id INTEGER PRIMARY KEY AUTOINCREMENT,
11
name TEXT NOT NULL,
12
price INTEGER NOT NULL,
13
category TEXT
14
)
15
""")
16
17
# 주문 테이블
18
cur.execute("""
19
CREATE TABLE IF NOT EXISTS orders (
20
id INTEGER PRIMARY KEY AUTOINCREMENT,
21
menu_id INTEGER,
22
qty INTEGER DEFAULT 1,
23
order_dt TEXT DEFAULT (date('now')),
24
FOREIGN KEY (menu_id) REFERENCES menus(id)
25
)
26
""")
27
28
conn.commit()
29
30
# 메뉴 데이터
31
menus_data = [
32
("아메리카노", 3500, "커피"),
33
("카페라떼", 4000, "커피"),
34
("바닐라라떼", 4500, "커피"),
35
("녹차라떼", 4500, "논커피"),
36
("딸기스무디", 5000, "논커피"),
37
("크루아상", 3500, "베이커리"),
38
("치즈케이크", 5500, "베이커리"),
39
("아이스티", 3000, "논커피"), # 주문이 없는 메뉴
40
]
41
cur.executemany("INSERT INTO menus (name,price,category) VALUES (?,?,?)", menus_data)
42
43
# 주문 데이터 (아이스티는 주문 없음)
44
orders_data = [
45
(1, 2), (1, 1), (2, 1), (3, 1),
46
(1, 3), (4, 1), (5, 2), (2, 1),
47
(6, 1), (7, 1), (1, 2),
48
]
49
cur.executemany("INSERT INTO orders (menu_id, qty) VALUES (?,?)", orders_data)
50
51
conn.commit()
52
print("카페 DB 준비 완료")

3. LEFT JOIN(레프트 조인) 실습

3.1. INNER JOIN vs LEFT JOIN 비교

1
# INNER JOIN: 주문된 메뉴만 조회
2
df_inner = pd.read_sql("""
3
SELECT m.name AS 메뉴, COUNT(o.id) AS 주문횟수
4
FROM orders AS o
5
INNER JOIN menus AS m ON o.menu_id = m.id
6
GROUP BY m.id, m.name
7
""", conn)
8
print("=== INNER JOIN 결과 (주문된 메뉴만) ===")
9
print(df_inner)
1
# LEFT JOIN: 주문 없는 메뉴도 포함
2
df_left = pd.read_sql("""
3
SELECT m.name AS 메뉴, COUNT(o.id) AS 주문횟수
4
FROM menus AS m
5
LEFT JOIN orders AS o ON m.id = o.menu_id
6
GROUP BY m.id, m.name
7
ORDER BY 주문횟수 DESC
8
""", conn)
9
print("=== LEFT JOIN 결과 (전체 메뉴 포함) ===")
10
print(df_left)

코드 설명:

  • INNER JOIN 결과: 아이스티는 주문이 없으므로 결과에 포함되지 않는다.
  • LEFT JOIN 결과: FROM menus — menus가 왼쪽 테이블이므로 모든 메뉴가 포함된다.
  • 아이스티는 orders에 매칭되는 행이 없어 주문횟수가 0으로 표시된다.

3.2. 주문 없는 메뉴 찾기

1
# 한 번도 주문되지 않은 메뉴 조회
2
df = pd.read_sql("""
3
SELECT m.name AS 메뉴, m.category AS 카테고리
4
FROM menus AS m
5
LEFT JOIN orders AS o ON m.id = o.menu_id
6
WHERE o.id IS NULL
7
""", conn)
8
print("=== 주문 없는 메뉴 ===")
9
print(df)

코드 설명:

  • 5행: LEFT JOIN으로 연결 후, 오른쪽 테이블(orders)의 id가 NULL인 행은 주문이 없는 메뉴이다.
  • 6행: WHERE o.id IS NULL — 일치하는 주문이 없는 행만 필터링한다.
  • 이 패턴은 “어떤 데이터가 다른 테이블에 없는지”를 찾을 때 사용된다.

4. SELF JOIN(셀프 조인) 실습

SELF JOIN은 같은 테이블을 자기 자신과 JOIN하는 방식이다. 계층 구조(상사-부하, 카테고리-서브카테고리 등)를 표현할 때 사용한다.

4.1. 직원-관리자 관계 예시

1
cur.execute("""
2
CREATE TABLE IF NOT EXISTS employees (
3
id INTEGER PRIMARY KEY AUTOINCREMENT,
4
name TEXT NOT NULL,
5
position TEXT,
6
manager_id INTEGER
7
)
8
""")
9
10
emp_data = [
11
("박대표", "대표이사", None),
12
("김팀장", "팀장", 1),
13
("이팀장", "팀장", 1),
14
("최직원", "사원", 2),
15
("강직원", "사원", 2),
16
("정직원", "사원", 3),
17
]
18
cur.executemany("INSERT INTO employees (name,position,manager_id) VALUES (?,?,?)", emp_data)
19
conn.commit()

코드 설명:

  • 6행: manager_id — 자신의 상사(관리자)의 id를 저장한다.
  • 11행: None — 대표이사는 상사가 없으므로 NULL이다.

4.2. SELF JOIN으로 상사 이름 조회

1
df = pd.read_sql("""
2
SELECT e.name AS 직원,
3
e.position AS 직급,
4
m.name AS 상사
5
FROM employees AS e
6
LEFT JOIN employees AS m ON e.manager_id = m.id
7
""", conn)
8
print("=== 직원-상사 관계 ===")
9
print(df)

코드 설명:

  • 5~6행: 같은 employees 테이블을 e(직원)와 m(관리자)이라는 두 별칭으로 사용한다.
  • e.manager_id = m.id — 직원의 manager_id가 관리자의 id와 같은 행을 연결한다.
  • LEFT JOIN 사용: 대표이사는 상사가 없어도(NULL) 결과에 포함시킨다.

5. LEFT JOIN 심화 실습

1
# 카테고리별 메뉴 수 & 총 주문횟수
2
df = pd.read_sql("""
3
SELECT m.category AS 카테고리,
4
COUNT(DISTINCT m.id) AS 메뉴수,
5
COUNT(o.id) AS 총주문수
6
FROM menus AS m
7
LEFT JOIN orders AS o ON m.id = o.menu_id
8
GROUP BY m.category
9
ORDER BY 총주문수 DESC
10
""", conn)
11
print("=== 카테고리별 통계 ===")
12
print(df)

코드 설명:

  • 4행: COUNT(DISTINCT m.id) — 중복 없이 메뉴 수를 센다.
  • 5행: COUNT(o.id) — 주문 횟수를 센다. NULL인 경우(주문 없는 메뉴)는 세지 않는다.

6. 활용 Plus — 리액트 연동 관점

리액트 카페 앱의 “메뉴별 판매 현황” 페이지에서 파이썬 API는 LEFT JOIN을 사용한다.

1
def get_sales():
2
df = pd.read_sql("""
3
SELECT m.name AS name,
4
m.category AS category,
5
m.price AS price,
6
COALESCE(SUM(o.qty), 0) AS total_sold
7
FROM menus AS m
8
LEFT JOIN orders AS o ON m.id = o.menu_id
9
GROUP BY m.id
10
ORDER BY total_sold DESC
11
""", conn)
12
return df.to_dict("records")
13
14
print(get_sales())

코드 설명:

  • 7행: COALESCE(SUM(o.qty), 0) — SUM 결과가 NULL(주문 없음)이면 0으로 대체한다.
  • COALESCE(코얼레스) — NULL 대신 지정한 기본값을 반환하는 함수이다.

7. 문제풀기

INNER JOIN: 두 테이블 모두에서 조건이 일치하는 행만 반환한다. LEFT JOIN: 왼쪽 테이블의 모든 행을 반환하고, 오른쪽 테이블에 일치하는 값이 없으면 NULL로 채운다.

오른쪽 테이블에 일치하는 데이터가 없는 왼쪽 테이블의 행을 찾는다. 즉, “아직 주문되지 않은 메뉴”, “활동이 없는 회원” 같은 경우를 찾을 때 사용한다.

같은 테이블 안에서 계층 관계를 표현할 때 사용한다. 예: 직원-상사 관계, 카테고리-서브카테고리, 친구-친구 관계 등

NULL 값을 지정한 기본값으로 대체한다. COALESCE(SUM(qty), 0)은 SUM 결과가 NULL이면 0을 반환한다.