11 SQL INNER JOIN 실습
1. 인트로
JOIN(조인)은 두 개 이상의 테이블을 연결하여 함께 조회하는 기능이다. 실제 서비스 DB는 데이터를 여러 테이블에 나눠 저장하기 때문에 JOIN은 필수 기술이다. 이번 차시에서는 INNER JOIN(이너 조인)을 중점적으로 익힌다.
2. 실습 DB 준비
1import sqlite32import pandas as pd3
4conn = sqlite3.connect("school.db")5cur = conn.cursor()6
7# 학생 테이블8cur.execute("""9 CREATE TABLE IF NOT EXISTS students (10 id INTEGER PRIMARY KEY AUTOINCREMENT,11 name TEXT NOT NULL,12 class_id INTEGER,13 grade INTEGER14 )15""")16
17# 반 테이블18cur.execute("""19 CREATE TABLE IF NOT EXISTS classes (20 id INTEGER PRIMARY KEY AUTOINCREMENT,21 name TEXT NOT NULL,22 teacher TEXT,23 room TEXT24 )25""")26
27# 과목 테이블28cur.execute("""29 CREATE TABLE IF NOT EXISTS subjects (30 id INTEGER PRIMARY KEY AUTOINCREMENT,31 name TEXT NOT NULL32 )33""")34
35# 성적 테이블36cur.execute("""37 CREATE TABLE IF NOT EXISTS scores (38 id INTEGER PRIMARY KEY AUTOINCREMENT,39 student_id INTEGER,40 subject_id INTEGER,41 score INTEGER,42 FOREIGN KEY (student_id) REFERENCES students(id),43 FOREIGN KEY (subject_id) REFERENCES subjects(id)44 )45""")46
47conn.commit()코드 설명:
- 8~14행: students 테이블. class_id로 classes 테이블과 연결된다.
- 17~23행: classes 테이블. 반 정보를 저장한다.
- 26~30행: subjects 테이블. 과목 정보를 저장한다.
- 33~41행: scores 테이블. 학생 id와 과목 id를 외래키로 연결하여 성적을 저장한다.
3. 샘플 데이터 삽입
1# 반 데이터2classes_data = [3 ("1반", "김선생님", "101호"),4 ("2반", "이선생님", "102호"),5 ("3반", "박선생님", "103호"),6]7cur.executemany("INSERT INTO classes (name,teacher,room) VALUES (?,?,?)", classes_data)8
9# 학생 데이터10students_data = [11 ("김철수", 1, 2), # 1반, 2학년12 ("이영희", 1, 2),13 ("박지민", 2, 2),14 ("최수진", 2, 2),15 ("홍길동", 3, 2),16 ("강민준", 3, 2),17]18cur.executemany("INSERT INTO students (name,class_id,grade) VALUES (?,?,?)", students_data)19
20# 과목 데이터21subjects_data = [("국어",), ("영어",), ("수학",), ("과학",)]22cur.executemany("INSERT INTO subjects (name) VALUES (?)", subjects_data)23
24# 성적 데이터25scores_data = [26 (1, 1, 85), (1, 2, 90), (1, 3, 78),27 (2, 1, 92), (2, 2, 88), (2, 3, 95),28 (3, 1, 70), (3, 2, 75), (3, 3, 80),29 (4, 1, 88), (4, 2, 91), (4, 3, 86),30 (5, 1, 60), (5, 2, 65), (5, 3, 72),31 (6, 1, 95), (6, 2, 93), (6, 3, 97),32]33cur.executemany("INSERT INTO scores (student_id,subject_id,score) VALUES (?,?,?)", scores_data)34
35conn.commit()36print("샘플 데이터 삽입 완료")4. INNER JOIN(이너 조인) 실습
4.1. INNER JOIN이란?
두 테이블에서 공통으로 일치하는 값이 있는 행만 연결하여 가져온다. 일치하지 않는 행은 결과에서 제외된다.
1students ──(class_id = classes.id)──▶ classes2두 테이블 모두에 일치하는 값이 있는 경우만 결과에 포함4.2. 기본 INNER JOIN
1# 학생 정보와 반 정보를 함께 조회2df = pd.read_sql("""3 SELECT s.name AS 학생이름,4 c.name AS 반이름,5 c.teacher AS 담임선생님6 FROM students AS s7 INNER JOIN classes AS c ON s.class_id = c.id8 ORDER BY c.name9""", conn)10print(df)코드 설명:
- 3행:
AS 학생이름— 컬럼의 별칭(Alias(앨리어스))을 지정한다. 결과에 표시될 이름이다. - 6행:
FROM students AS s— students 테이블을s라는 짧은 별칭으로 사용한다. - 7행:
INNER JOIN classes AS c ON s.class_id = c.id— students의 class_id와 classes의 id가 같은 행을 연결한다.
4.3. DB 테이블 별명 지정 & USING 활용
1# USING으로 JOIN (컬럼명이 같을 때 사용 가능)2# 참고: 컬럼명이 다르면 ON을 사용한다3df = pd.read_sql("""4 SELECT scores.score,5 students.name AS 학생,6 subjects.name AS 과목7 FROM scores8 INNER JOIN students ON scores.student_id = students.id9 INNER JOIN subjects ON scores.subject_id = subjects.id10 ORDER BY students.name, subjects.name11""", conn)12print(df)코드 설명:
- 7~8행: scores 테이블을 두 테이블과 각각 JOIN한다. 이처럼 테이블 3개를 연결할 수 있다.
- 결과에는 성적, 학생 이름, 과목 이름이 함께 출력된다.
4.4. DB 테이블 중첩 JOIN 실습
1# 학생, 반, 성적, 과목을 모두 연결2df = pd.read_sql("""3 SELECT st.name AS 학생,4 c.name AS 반,5 su.name AS 과목,6 sc.score AS 점수7 FROM scores AS sc8 INNER JOIN students AS st ON sc.student_id = st.id9 INNER JOIN classes AS c ON st.class_id = c.id10 INNER JOIN subjects AS su ON sc.subject_id = su.id11 WHERE c.name = '1반'12 ORDER BY su.name13""", conn)14print("=== 1반 성적표 ===")15print(df)코드 설명:
- 7~10행: scores, students, classes, subjects 총 4개의 테이블을 연결한다.
- 11행:
WHERE c.name = '1반'— 최종 결과에서 1반만 필터링한다. - 이 한 번의 SQL로 4개 테이블의 정보를 통합하여 조회한다.
5. 집계 함수와 INNER JOIN 조합
1# 학생별 평균 점수 계산2df = pd.read_sql("""3 SELECT st.name AS 학생이름,4 AVG(sc.score) AS 평균점수,5 MAX(sc.score) AS 최고점수,6 MIN(sc.score) AS 최저점수7 FROM scores AS sc8 INNER JOIN students AS st ON sc.student_id = st.id9 GROUP BY st.id, st.name10 ORDER BY 평균점수 DESC11""", conn)12print("=== 학생별 성적 요약 ===")13print(df.round(1))코드 설명:
- 4행:
AVG(sc.score)— 각 학생의 성적 평균을 계산한다. - 9행:
GROUP BY st.id, st.name— 학생별로 묶어서 집계한다. (GROUP BY는 13차시에서 자세히 배운다.) - 10행:
ORDER BY 평균점수 DESC— 별칭(평균점수)으로도 정렬할 수 있다.
6. 활용 Plus — 리액트 연동 관점
리액트 학교 앱에서 “학생 성적표” 페이지를 만들 때, API 서버는 이 JOIN 쿼리를 실행한다.
1def get_report(class_name):2 df = pd.read_sql("""3 SELECT st.name AS 학생, su.name AS 과목, sc.score AS 점수4 FROM scores AS sc5 INNER JOIN students AS st ON sc.student_id = st.id6 INNER JOIN classes AS c ON st.class_id = c.id7 INNER JOIN subjects AS su ON sc.subject_id = su.id8 WHERE c.name = ?9 ORDER BY st.name, su.name10 """, conn, params=(class_name,))11 return df.to_dict("records")12
13data = get_report("1반")14print(data[:3])코드 설명:
- 10행:
params=(class_name,)— pandas read_sql에서 파라미터를 전달하는 방법이다. - 11행:
.to_dict("records")— JSON 형태로 변환하여 리액트에 반환한다.
7. 문제풀기
두 테이블에서 JOIN 조건이 일치하는 행만 결과에 포함된다. 한쪽 테이블에만 있는 행은 제외된다.
테이블명이 길 때 짧은 별칭으로 코드를 간결하게 만들기 위해서이다. 또한 같은 테이블을 두 번 조인할 때 구분하기 위해서도 사용한다.
ON scores.student_id = students.id scores 테이블의 student_id가 students 테이블의 id와 같은 행을 연결한다.
각 JOIN마다 ON 조건을 정확하게 지정해야 한다. 잘못된 ON 조건은 카르테시안 곱(모든 행의 조합)을 만들어 엄청난 양의 잘못된 결과를 반환한다.