[Python 100일 챌린지] Day 76 - SQL 고급: JOIN으로 테이블 연결하기
여러 테이블의 데이터를 연결하는 JOIN을 배워봅시다! 실무 데이터베이스는 하나의 큰 테이블이 아니라 여러 작은 테이블로 나누어져 있습니다. JOIN을 마스터하면 복잡한 관계형 데이터를 자유자재로 다룰 수 있어요! (30분 완독 ⭐⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
🎯 학습 목표 1: 테이블 관계와 외래 키 이해하기
한 줄 설명
관계형 데이터베이스 = 여러 테이블을 연결해서 사용하는 데이터베이스
하나의 큰 테이블 대신, 관련된 데이터를 여러 테이블로 나누고 관계를 맺습니다.
왜 테이블을 나누나요?
나쁜 예 - 하나의 큰 테이블:
1
2
3
4
5
6
7
주문 테이블 (모든 정보를 한 곳에)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
주문ID | 고객명 | 고객주소 | 상품명 | 가격
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 | 홍길동 | 서울 | 노트북 | 1200000
2 | 홍길동 | 서울 | 마우스 | 25000
3 | 김철수 | 부산 | 키보드 | 80000
문제점:
- 홍길동의 주소가 중복 저장됨 (데이터 낭비)
- 주소 변경 시 여러 곳을 수정해야 함 (불일치 위험)
좋은 예 - 테이블 분리:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
고객 테이블
━━━━━━━━━━━━━━━━━━
고객ID | 이름 | 주소
━━━━━━━━━━━━━━━━━━
1 | 홍길동 | 서울
2 | 김철수 | 부산
상품 테이블
━━━━━━━━━━━━━━━━━━━━
상품ID | 상품명 | 가격
━━━━━━━━━━━━━━━━━━━━
101 | 노트북 | 1200000
102 | 마우스 | 25000
103 | 키보드 | 80000
주문 테이블
━━━━━━━━━━━━━━━━━━━━━━
주문ID | 고객ID | 상품ID
━━━━━━━━━━━━━━━━━━━━━━
1 | 1 | 101
2 | 1 | 102
3 | 2 | 103
외래 키(Foreign Key)란?
외래 키 = 다른 테이블의 기본 키를 참조하는 컬럼
erDiagram
CUSTOMER ||--o{ ORDER : places
PRODUCT ||--o{ ORDER : "ordered in"
CUSTOMER {
int customer_id PK
string name
string address
}
PRODUCT {
int product_id PK
string name
int price
}
ORDER {
int order_id PK
int customer_id FK
int product_id FK
}
Python에서 관계형 테이블 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# 1. 고객 테이블
cursor.execute('''
CREATE TABLE IF NOT EXISTS customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
address TEXT
)
''')
# 2. 상품 테이블
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
product_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL
)
''')
# 3. 주문 테이블 (외래 키 포함)
cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
product_id INTEGER,
quantity INTEGER DEFAULT 1,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
)
''')
# 샘플 데이터 삽입
customers = [
(1, '홍길동', '서울'),
(2, '김철수', '부산'),
(3, '이영희', '대구')
]
products = [
(101, '노트북', 1200000),
(102, '마우스', 25000),
(103, '키보드', 80000),
(104, '모니터', 350000)
]
orders = [
(1, 1, 101, 1), # 홍길동이 노트북 1개 주문
(2, 1, 102, 2), # 홍길동이 마우스 2개 주문
(3, 2, 103, 1), # 김철수가 키보드 1개 주문
(4, 2, 104, 1), # 김철수가 모니터 1개 주문
]
cursor.executemany('INSERT OR REPLACE INTO customers VALUES (?, ?, ?)', customers)
cursor.executemany('INSERT OR REPLACE INTO products VALUES (?, ?, ?)', products)
cursor.executemany('INSERT OR REPLACE INTO orders VALUES (?, ?, ?, ?)', orders)
conn.commit()
print("테이블 생성 및 데이터 삽입 완료!")
conn.close()
🎯 학습 목표 2: INNER JOIN으로 교집합 찾기
한 줄 설명
INNER JOIN = 두 테이블에서 조건이 일치하는 행만 결합
양쪽 테이블 모두에 데이터가 있을 때만 결과에 포함됩니다.
INNER JOIN 기본 문법
1
2
3
4
SELECT 테이블1.컬럼, 테이블2.컬럼
FROM 테이블1
INNER JOIN 테이블2
ON 테이블1.공통컬럼 = 테이블2.공통컬럼;
시각적 이해
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
고객 테이블 주문 테이블
━━━━━━━━━━ ━━━━━━━━━━
고객ID | 이름 주문ID | 고객ID
━━━━━━━━━━ ━━━━━━━━━━
1 | 홍길동 1 | 1
2 | 김철수 2 | 1
3 | 이영희 3 | 2
INNER JOIN 결과 (고객ID 일치하는 것만)
━━━━━━━━━━━━━━━━━━━━━━━━━━━
고객ID | 이름 | 주문ID
━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 | 홍길동 | 1
1 | 홍길동 | 2
2 | 김철수 | 3
👉 이영희는 주문이 없어서 결과에 없음!
Python에서 INNER JOIN 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# 예제 1: 주문 정보와 고객 이름 함께 조회
query = '''
SELECT
orders.order_id,
customers.name AS customer_name,
orders.quantity
FROM orders
INNER JOIN customers
ON orders.customer_id = customers.customer_id
'''
cursor.execute(query)
print("=== 주문 내역 (고객명 포함) ===")
for row in cursor.fetchall():
print(f"주문 #{row[0]}: {row[1]}님 - 수량 {row[2]}개")
conn.close()
출력:
1
2
3
4
5
=== 주문 내역 (고객명 포함) ===
주문 #1: 홍길동님 - 수량 1개
주문 #2: 홍길동님 - 수량 2개
주문 #3: 김철수님 - 수량 1개
주문 #4: 김철수님 - 수량 1개
여러 컬럼 JOIN하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# 주문, 고객, 상품 정보 모두 가져오기
query = '''
SELECT
orders.order_id,
customers.name AS customer_name,
products.name AS product_name,
products.price,
orders.quantity,
(products.price * orders.quantity) AS total_price
FROM orders
INNER JOIN customers ON orders.customer_id = customers.customer_id
INNER JOIN products ON orders.product_id = products.product_id
'''
cursor.execute(query)
print("=== 상세 주문 내역 ===")
for row in cursor.fetchall():
print(f"주문 #{row[0]}")
print(f" 고객: {row[1]}")
print(f" 상품: {row[2]}")
print(f" 단가: {row[3]:,}원")
print(f" 수량: {row[4]}개")
print(f" 총액: {row[5]:,}원")
print()
conn.close()
출력:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
=== 상세 주문 내역 ===
주문 #1
고객: 홍길동
상품: 노트북
단가: 1,200,000원
수량: 1개
총액: 1,200,000원
주문 #2
고객: 홍길동
상품: 마우스
단가: 25,000원
수량: 2개
총액: 50,000원
주문 #3
고객: 김철수
상품: 키보드
단가: 80,000원
수량: 1개
총액: 80,000원
주문 #4
고객: 김철수
상품: 모니터
단가: 350,000원
수량: 1개
총액: 350,000원
🎯 학습 목표 3: LEFT JOIN으로 왼쪽 데이터 모두 가져오기
한 줄 설명
LEFT JOIN = 왼쪽 테이블의 모든 행 + 오른쪽 테이블의 일치하는 행
왼쪽 테이블 데이터는 모두 포함되고, 오른쪽에 일치하는 데이터가 없으면 NULL로 표시됩니다.
LEFT JOIN 기본 문법
1
2
3
4
SELECT 테이블1.컬럼, 테이블2.컬럼
FROM 테이블1
LEFT JOIN 테이블2
ON 테이블1.공통컬럼 = 테이블2.공통컬럼;
시각적 이해
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
고객 테이블 주문 테이블
━━━━━━━━━━ ━━━━━━━━━━
고객ID | 이름 주문ID | 고객ID
━━━━━━━━━━ ━━━━━━━━━━
1 | 홍길동 1 | 1
2 | 김철수 2 | 1
3 | 이영희 3 | 2
LEFT JOIN 결과 (고객 테이블 기준 모두)
━━━━━━━━━━━━━━━━━━━━━━━━━━━
고객ID | 이름 | 주문ID
━━━━━━━━━━━━━━━━━━━━━━━━━━━
1 | 홍길동 | 1
1 | 홍길동 | 2
2 | 김철수 | 3
3 | 이영희 | NULL
👉 이영희도 포함! (주문ID는 NULL)
Python에서 LEFT JOIN 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# 모든 고객과 주문 내역 (주문 없는 고객도 포함)
query = '''
SELECT
customers.customer_id,
customers.name,
COUNT(orders.order_id) AS order_count
FROM customers
LEFT JOIN orders
ON customers.customer_id = orders.customer_id
GROUP BY customers.customer_id, customers.name
'''
cursor.execute(query)
print("=== 고객별 주문 수 ===")
for row in cursor.fetchall():
print(f"{row[1]}님: {row[2]}건")
conn.close()
출력:
1
2
3
4
=== 고객별 주문 수 ===
홍길동님: 2건
김철수님: 2건
이영희님: 0건 # 주문이 없어도 표시됨!
LEFT JOIN으로 NULL 찾기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# 한 번도 주문하지 않은 고객 찾기
query = '''
SELECT
customers.customer_id,
customers.name,
customers.address
FROM customers
LEFT JOIN orders
ON customers.customer_id = orders.customer_id
WHERE orders.order_id IS NULL
'''
cursor.execute(query)
print("=== 주문 이력이 없는 고객 ===")
for row in cursor.fetchall():
print(f"ID {row[0]}: {row[1]} ({row[2]})")
conn.close()
출력:
1
2
=== 주문 이력이 없는 고객 ===
ID 3: 이영희 (대구)
RIGHT JOIN 개념 (참고)
RIGHT JOIN = 오른쪽 테이블의 모든 행 + 왼쪽 테이블의 일치하는 행
💡 주의: SQLite는 RIGHT JOIN을 지원하지 않습니다! 대신 LEFT JOIN에서 테이블 순서를 바꾸면 같은 효과를 낼 수 있어요.
1
2
3
4
5
6
7
8
9
10
# RIGHT JOIN 효과 내기 (테이블 순서 바꾸기)
# RIGHT JOIN orders (원래 의도)
# ↓
# LEFT JOIN으로 변경
query = '''
SELECT *
FROM orders
LEFT JOIN customers
ON orders.customer_id = customers.customer_id
'''
🎯 학습 목표 4: 여러 테이블 JOIN하기
3개 이상 테이블 JOIN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# 주문, 고객, 상품 3개 테이블 JOIN
query = '''
SELECT
customers.name AS customer_name,
customers.address,
products.name AS product_name,
products.price,
orders.quantity,
(products.price * orders.quantity) AS total
FROM orders
INNER JOIN customers ON orders.customer_id = customers.customer_id
INNER JOIN products ON orders.product_id = products.product_id
ORDER BY customers.name, orders.order_id
'''
cursor.execute(query)
print("=== 고객별 주문 상세 ===")
current_customer = None
customer_total = 0
for row in cursor.fetchall():
customer_name = row[0]
# 새로운 고객이면 이전 고객 총액 출력
if current_customer and current_customer != customer_name:
print(f" {current_customer}님 주문 총액: {customer_total:,}원\n")
customer_total = 0
if current_customer != customer_name:
print(f"[{customer_name}님 - {row[1]}]")
current_customer = customer_name
print(f" - {row[2]}: {row[4]}개 × {row[3]:,}원 = {row[5]:,}원")
customer_total += row[5]
# 마지막 고객 총액
if current_customer:
print(f" {current_customer}님 주문 총액: {customer_total:,}원")
conn.close()
출력:
1
2
3
4
5
6
7
8
9
10
=== 고객별 주문 상세 ===
[김철수님 - 부산]
- 키보드: 1개 × 80,000원 = 80,000원
- 모니터: 1개 × 350,000원 = 350,000원
김철수님 주문 총액: 430,000원
[홍길동님 - 서울]
- 노트북: 1개 × 1,200,000원 = 1,200,000원
- 마우스: 2개 × 25,000원 = 50,000원
홍길동님 주문 총액: 1,250,000원
실전 예제: 주문 통계 시스템
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import sqlite3
def get_order_statistics(db_path):
"""주문 통계를 분석하는 함수"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 1. 고객별 주문 통계
print("=== 고객별 주문 통계 ===")
query1 = '''
SELECT
customers.name,
COUNT(orders.order_id) AS order_count,
SUM(products.price * orders.quantity) AS total_amount
FROM customers
LEFT JOIN orders ON customers.customer_id = orders.customer_id
LEFT JOIN products ON orders.product_id = products.product_id
GROUP BY customers.customer_id, customers.name
ORDER BY total_amount DESC
'''
cursor.execute(query1)
for row in cursor.fetchall():
name, count, total = row
total = total if total else 0
print(f"{name}님: {count}건, 총액 {total:,}원")
# 2. 인기 상품 TOP 3
print("\n=== 인기 상품 TOP 3 ===")
query2 = '''
SELECT
products.name,
SUM(orders.quantity) AS total_sold,
COUNT(orders.order_id) AS order_count
FROM products
LEFT JOIN orders ON products.product_id = orders.product_id
GROUP BY products.product_id, products.name
ORDER BY total_sold DESC
LIMIT 3
'''
cursor.execute(query2)
for i, row in enumerate(cursor.fetchall(), 1):
name, sold, count = row
sold = sold if sold else 0
count = count if count else 0
print(f"{i}위: {name} - {sold}개 판매, {count}건 주문")
# 3. 지역별 매출 통계
print("\n=== 지역별 매출 ===")
query3 = '''
SELECT
customers.address,
COUNT(orders.order_id) AS order_count,
SUM(products.price * orders.quantity) AS total_sales
FROM customers
LEFT JOIN orders ON customers.customer_id = orders.customer_id
LEFT JOIN products ON orders.product_id = products.product_id
GROUP BY customers.address
ORDER BY total_sales DESC
'''
cursor.execute(query3)
for row in cursor.fetchall():
address, count, sales = row
sales = sales if sales else 0
print(f"{address}: {count}건, {sales:,}원")
conn.close()
# 실행
get_order_statistics('shop.db')
출력:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== 고객별 주문 통계 ===
홍길동님: 2건, 총액 1,250,000원
김철수님: 2건, 총액 430,000원
이영희님: 0건, 총액 0원
=== 인기 상품 TOP 3 ===
1위: 마우스 - 2개 판매, 1건 주문
2위: 노트북 - 1개 판매, 1건 주문
3위: 키보드 - 1개 판매, 1건 주문
=== 지역별 매출 ===
서울: 2건, 1,250,000원
부산: 2건, 430,000원
대구: 0건, 0원
💡 실전 팁 & 주의사항
💡 Tip 1: 테이블 별칭(Alias) 사용하기
1
2
3
4
5
6
7
8
9
10
# 긴 테이블 이름을 짧게
query = '''
SELECT
c.name,
p.name,
o.quantity
FROM orders AS o
INNER JOIN customers AS c ON o.customer_id = c.customer_id
INNER JOIN products AS p ON o.product_id = p.product_id
'''
💡 Tip 2: 명시적 컬럼 이름 사용
1
2
3
4
5
# ❌ 모호한 코드
query = 'SELECT name FROM customers JOIN products ...' # 어느 테이블의 name?
# ✅ 명확한 코드
query = 'SELECT customers.name, products.name FROM customers JOIN products ...'
💡 Tip 3: JOIN 조건 확인하기
1
2
3
4
5
6
7
8
9
# 잘못된 JOIN 조건은 엄청난 데이터 폭발 유발!
# ❌ ON 조건 없음 - Cartesian Product (모든 조합)
query = 'SELECT * FROM customers, orders' # 3 × 4 = 12행!
# ✅ 올바른 JOIN
query = '''
SELECT * FROM customers
INNER JOIN orders ON customers.customer_id = orders.customer_id
''' # 4행 (실제 주문 건수)
💡 Tip 4: NULL 처리하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# COALESCE로 NULL을 기본값으로 대체
query = '''
SELECT
customers.name,
COALESCE(COUNT(orders.order_id), 0) AS order_count,
COALESCE(SUM(products.price * orders.quantity), 0) AS total
FROM customers
LEFT JOIN orders ON customers.customer_id = orders.customer_id
LEFT JOIN products ON orders.product_id = products.product_id
GROUP BY customers.customer_id, customers.name
'''
cursor.execute(query)
for row in cursor.fetchall():
print(f"{row[0]}님: {row[1]}건, {row[2]:,}원")
conn.close()
💡 Tip 5: JOIN 성능 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sqlite3
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
# 인덱스 생성으로 JOIN 성능 향상
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_customer ON orders(customer_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_product ON orders(product_id)')
conn.commit()
print("인덱스 생성 완료!")
# 이제 JOIN이 훨씬 빠릅니다!
conn.close()
🧪 연습 문제
문제 1: 도서관 관리 시스템
과제: 도서관의 대출 기록을 관리하는 시스템을 만드세요.
테이블 구조:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# books (도서)
books = [
(1, 'Python 입문', '김파이썬'),
(2, 'SQL 마스터', '박데이터'),
(3, 'Django 실전', '이웹'),
(4, 'Flask 가이드', '최개발')
]
# members (회원)
members = [
(1, '홍길동', '서울'),
(2, '김철수', '부산'),
(3, '이영희', '대구')
]
# rentals (대출)
rentals = [
(1, 1, 1, '2024-05-01', '2024-05-15'), # 홍길동, Python 입문
(2, 1, 2, '2024-05-05', '2024-05-19'), # 홍길동, SQL 마스터
(3, 2, 3, '2024-05-10', None) # 김철수, Django 실전 (미반납)
]
요구사항:
- 테이블 3개 생성 및 데이터 삽입
- 모든 대출 기록 조회 (회원명, 도서명, 대출일, 반납일 포함)
- 반납하지 않은 도서 목록 (회원명, 도서명, 대출일)
- 회원별 대출 통계 (회원명, 대출 건수)
- 한 번도 대출되지 않은 도서 목록
💡 힌트
- 3개 테이블 JOIN:
rentals JOIN members JOIN books - 미반납:
WHERE return_date IS NULL - 대출 건수:
COUNT(rental_id) - 미대출 도서:
LEFT JOIN+WHERE rental_id IS NULL
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import sqlite3
conn = sqlite3.connect('library.db')
cursor = conn.cursor()
# 1. 테이블 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS books (
book_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS members (
member_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
address TEXT
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS rentals (
rental_id INTEGER PRIMARY KEY,
member_id INTEGER,
book_id INTEGER,
rental_date TEXT NOT NULL,
return_date TEXT,
FOREIGN KEY (member_id) REFERENCES members(member_id),
FOREIGN KEY (book_id) REFERENCES books(book_id)
)
''')
# 데이터 삽입
books = [
(1, 'Python 입문', '김파이썬'),
(2, 'SQL 마스터', '박데이터'),
(3, 'Django 실전', '이웹'),
(4, 'Flask 가이드', '최개발')
]
members = [
(1, '홍길동', '서울'),
(2, '김철수', '부산'),
(3, '이영희', '대구')
]
rentals = [
(1, 1, 1, '2024-05-01', '2024-05-15'),
(2, 1, 2, '2024-05-05', '2024-05-19'),
(3, 2, 3, '2024-05-10', None)
]
cursor.executemany('INSERT OR REPLACE INTO books VALUES (?, ?, ?)', books)
cursor.executemany('INSERT OR REPLACE INTO members VALUES (?, ?, ?)', members)
cursor.executemany('INSERT OR REPLACE INTO rentals VALUES (?, ?, ?, ?, ?)', rentals)
conn.commit()
# 2. 모든 대출 기록
print("=== 전체 대출 기록 ===")
query = '''
SELECT
members.name,
books.title,
books.author,
rentals.rental_date,
COALESCE(rentals.return_date, '미반납') AS return_date
FROM rentals
INNER JOIN members ON rentals.member_id = members.member_id
INNER JOIN books ON rentals.book_id = books.book_id
ORDER BY rentals.rental_date
'''
cursor.execute(query)
for row in cursor.fetchall():
print(f"{row[0]}님 - 《{row[1]}》 ({row[2]}) | 대출: {row[3]}, 반납: {row[4]}")
# 3. 미반납 도서
print("\n=== 미반납 도서 ===")
query = '''
SELECT
members.name,
books.title,
rentals.rental_date
FROM rentals
INNER JOIN members ON rentals.member_id = members.member_id
INNER JOIN books ON rentals.book_id = books.book_id
WHERE rentals.return_date IS NULL
'''
cursor.execute(query)
for row in cursor.fetchall():
print(f"{row[0]}님 - 《{row[1]}》 (대출일: {row[2]})")
# 4. 회원별 대출 통계
print("\n=== 회원별 대출 통계 ===")
query = '''
SELECT
members.name,
COUNT(rentals.rental_id) AS rental_count
FROM members
LEFT JOIN rentals ON members.member_id = rentals.member_id
GROUP BY members.member_id, members.name
ORDER BY rental_count DESC
'''
cursor.execute(query)
for row in cursor.fetchall():
print(f"{row[0]}님: {row[1]}건")
# 5. 미대출 도서
print("\n=== 한 번도 대출되지 않은 도서 ===")
query = '''
SELECT
books.title,
books.author
FROM books
LEFT JOIN rentals ON books.book_id = rentals.book_id
WHERE rentals.rental_id IS NULL
'''
cursor.execute(query)
for row in cursor.fetchall():
print(f"《{row[0]}》 - {row[1]}")
conn.close()
출력:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
=== 전체 대출 기록 ===
홍길동님 - 《Python 입문》 (김파이썬) | 대출: 2024-05-01, 반납: 2024-05-15
홍길동님 - 《SQL 마스터》 (박데이터) | 대출: 2024-05-05, 반납: 2024-05-19
김철수님 - 《Django 실전》 (이웹) | 대출: 2024-05-10, 반납: 미반납
=== 미반납 도서 ===
김철수님 - 《Django 실전》 (대출일: 2024-05-10)
=== 회원별 대출 통계 ===
홍길동님: 2건
김철수님: 1건
이영희님: 0건
=== 한 번도 대출되지 않은 도서 ===
《Flask 가이드》 - 최개발
📝 오늘 배운 내용 정리
- 관계형 DB: 여러 테이블로 데이터 분리, 외래 키로 연결
- INNER JOIN: 양쪽 테이블에 모두 존재하는 데이터만 결합
- LEFT JOIN: 왼쪽 테이블 모두 + 오른쪽 일치하는 데이터
- 여러 테이블 JOIN: 3개 이상 테이블도 연결 가능
- 실전 활용: 통계, 분석, 보고서 생성에 필수
🔗 관련 자료
📚 이전 학습
Day 75: SQL 기본 - UPDATE, DELETE ⭐⭐⭐
어제는 데이터를 수정하고 삭제하는 UPDATE와 DELETE를 배웠습니다!
📚 다음 학습
Day 77: SQL 고급 - 집계함수와 GROUP BY ⭐⭐⭐⭐
내일은 데이터를 집계하고 그룹화하는 고급 SQL 기능을 배웁니다!
“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀
Day 76/100 Phase 8: 데이터베이스와 SQL #100DaysOfPython
