🐨CoalaCoding
DocsExamplesTry itBoardB반
🐨CoalaCoding

개발자를 위한 한국어 웹 기술 문서

문서

  • JavaScript
  • Web Publishing
  • React
  • Python

커뮤니티

  • 게시판
  • 예제 모음
  • Try it 에디터

기타

  • GitHub
  • 관리자
© 2026 CoalaCoding. All rights reserved.
  • 01_sqlite3로 SQL 명령문 실행
  • 02_DB데이터 일괄추가하기
  • 03_SQLite3 라이브러리를 활용한 SQL 명령문 실행
  • 04_Pandas를 활용한 SQL 명령문 실행
  • 05_CREATE 명령어를 활용한 DB 테이블의 생성
  • 06_DDL 명령어의 이해 및 실습
  • 07_SQL SELECT 명령어의 이해 및 실습
  • 08_SQL INSERT, UPDATE, DELETE 명령어 실습
  • 09_SQL SELECT 세부 명령어 실습 1
  • 10_SQL SELECT 세부 명령어 실습 2
  • 11_SQL INNER JOIN 실습
  • 12_SQL LEFT JOIN 실습
  • 13_SQL GROUP BY 실습 1
  • 14_SQL GROUP BY 실습 2
  • 15_SQL SubQuery 활용 실습
  • 16_데이터셋 기반 DB 구축 & DB 활용 실습 — 리액트 연동 완성
  1. 홈
  2. 문서
  3. Backend
  4. Database & SQL
  5. 15_SQL SubQuery 활용 실습

15_SQL SubQuery 활용 실습

코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.

구문

1. 인트로

SubQuery(서브쿼리)는 SQL 안에 또 다른 SQL을 넣는 기술이다. 단일 SELECT로 처리하기 어려운 복잡한 조건을 하나의 쿼리로 해결할 수 있다. 이번 차시에서는 WHERE, FROM, SELECT 절에서의 서브쿼리 사용법을 모두 익힌다.


2. 실습 DB 준비

import sqlite3
import pandas as pd

conn = sqlite3.connect("streaming.db")
cur  = conn.cursor()

# 사용자 테이블
cur.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id       INTEGER PRIMARY KEY AUTOINCREMENT,
        name     TEXT    NOT NULL,
        plan     TEXT    DEFAULT 'basic',
        join_dt  TEXT    DEFAULT (date('now'))
    )
""")

# 콘텐츠 테이블
cur.execute("""
    CREATE TABLE IF NOT EXISTS contents (
        id       INTEGER PRIMARY KEY AUTOINCREMENT,
        title    TEXT    NOT NULL,
        genre    TEXT,
        rating   REAL
    )
""")

# 시청 기록 테이블
cur.execute("""
    CREATE TABLE IF NOT EXISTS watches (
        id         INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id    INTEGER,
        content_id INTEGER,
        watch_dt   TEXT DEFAULT (date('now')),
        FOREIGN KEY (user_id)    REFERENCES users(id),
        FOREIGN KEY (content_id) REFERENCES contents(id)
    )
""")

conn.commit()

# 데이터 삽입
users_data = [
    ("김철수", "premium"), ("이영희", "basic"),
    ("박지민", "premium"), ("최수진", "basic"),
    ("홍길동", "standard"),("강민준", "premium"),
]
cur.executemany("INSERT INTO users (name,plan) VALUES (?,?)", users_data)

contents_data = [
    ("오징어게임",    "드라마",  9.5),
    ("더글로리",     "드라마",  9.2),
    ("이상한변호사우영우", "드라마", 9.0),
    ("정이",        "SF",    7.8),
    ("수리남",       "스릴러", 8.5),
    ("D.P.",       "드라마",  8.8),
]
cur.executemany("INSERT INTO contents (title,genre,rating) VALUES (?,?,?)", contents_data)

watches_data = [
    (1,1),(1,2),(1,3),(1,5),
    (2,1),(2,4),
    (3,1),(3,2),(3,3),(3,4),(3,5),(3,6),
    (4,2),
    (5,1),(5,3),
    (6,1),(6,2),(6,5),(6,6),
]
cur.executemany("INSERT INTO watches (user_id,content_id) VALUES (?,?)", watches_data)

conn.commit()
print("스트리밍 DB 준비 완료")

3. WHERE 절에서의 SubQuery 활용법

3.1. 평균보다 높은 평점의 콘텐츠

df = pd.read_sql("""
    SELECT title, rating
    FROM contents
    WHERE rating > (SELECT AVG(rating) FROM contents)
    ORDER BY rating DESC
""", conn)
print("=== 평균 이상 평점 콘텐츠 ===")
print(df)

코드 설명:

  • 4행: (SELECT AVG(rating) FROM contents) — 전체 콘텐츠의 평균 평점을 구한다.
  • 바깥 WHERE에서 그 값보다 높은 콘텐츠만 필터링한다.

3.2. 가장 많이 시청된 콘텐츠 조회

cur.execute("""
    SELECT title, rating
    FROM contents
    WHERE id = (
        SELECT content_id
        FROM watches
        GROUP BY content_id
        ORDER BY COUNT(*) DESC
        LIMIT 1
    )
""")
top = cur.fetchone()
print(f"최다 시청 콘텐츠: {top[0]} (평점: {top[1]})")

코드 설명:

  • 5~9행: 서브쿼리에서 watches 테이블을 content_id로 그룹화하여 가장 많이 시청된 content_id를 구한다.
  • 바깥 쿼리에서 그 id와 일치하는 콘텐츠 정보를 가져온다.

3.3. IN + SubQuery 조합

# premium 사용자가 시청한 콘텐츠 목록
df = pd.read_sql("""
    SELECT DISTINCT c.title, c.genre
    FROM contents AS c
    WHERE c.id IN (
        SELECT w.content_id
        FROM watches AS w
        INNER JOIN users AS u ON w.user_id = u.id
        WHERE u.plan = 'premium'
    )
    ORDER BY c.title
""", conn)
print("=== 프리미엄 사용자 시청 콘텐츠 ===")
print(df)

코드 설명:

  • 5~10행: 서브쿼리에서 premium 사용자가 시청한 content_id 목록을 구한다.
  • 3행: DISTINCT — 중복 없이 고유한 콘텐츠만 가져온다.
  • 바깥 WHERE IN에서 그 목록에 포함된 콘텐츠만 가져온다.

4. FROM 절에서의 SubQuery 활용법

FROM 절에 서브쿼리를 넣으면 결과를 임시 테이블(Derived Table(더라이브드 테이블))처럼 사용할 수 있다.

4.1. 사용자별 시청 수 집계 후 필터링

df = pd.read_sql("""
    SELECT name, plan, watch_cnt
    FROM (
        SELECT u.name, u.plan, COUNT(w.id) AS watch_cnt
        FROM users AS u
        LEFT JOIN watches AS w ON u.id = w.user_id
        GROUP BY u.id, u.name, u.plan
    ) AS user_stats
    WHERE watch_cnt >= 3
    ORDER BY watch_cnt DESC
""", conn)
print("=== 시청 3개 이상 사용자 ===")
print(df)

코드 설명:

  • 3~8행: FROM 절 안의 서브쿼리로 사용자별 시청 수를 집계한다.
  • 8행: AS user_stats — 서브쿼리 결과에 이름을 붙인다. (필수)
  • 9행: 집계 결과인 watch_cnt로 필터링한다. HAVING 대신 WHERE를 사용할 수 있다.

5. SubQuery(서브쿼리) 심화 실습

5.1. 상관 서브쿼리(Correlated SubQuery(코릴레이티드 서브쿼리))

바깥 쿼리의 각 행마다 서브쿼리가 실행되는 패턴이다.

# 각 장르의 평균보다 높은 평점의 콘텐츠
df = pd.read_sql("""
    SELECT c1.title, c1.genre, c1.rating
    FROM contents AS c1
    WHERE c1.rating > (
        SELECT AVG(c2.rating)
        FROM contents AS c2
        WHERE c2.genre = c1.genre
    )
    ORDER BY c1.genre, c1.rating DESC
""", conn)
print("=== 장르 내 평균 초과 콘텐츠 ===")
print(df)

코드 설명:

  • 5~8행: 서브쿼리 안에서 c2.genre = c1.genre로 바깥 쿼리의 행(c1)을 참조한다.
  • 바깥 쿼리의 각 행(c1)마다 서브쿼리가 실행되어 해당 장르의 평균을 계산한다.
  • c1의 평점이 그 장르 평균보다 높은 경우만 결과에 포함한다.

5.2. EXISTS(이그지스츠)를 사용한 서브쿼리

# 시청 기록이 있는 사용자만 조회
df = pd.read_sql("""
    SELECT u.name, u.plan
    FROM users AS u
    WHERE EXISTS (
        SELECT 1
        FROM watches AS w
        WHERE w.user_id = u.id
    )
""", conn)
print("=== 시청 기록 있는 사용자 ===")
print(df)

코드 설명:

  • 5행: EXISTS — 서브쿼리의 결과가 한 행 이상 존재하면 TRUE를 반환한다.
  • 6행: SELECT 1 — 실제 값보다 존재 여부만 확인하므로 1을 사용한다. 성능상 효율적이다.
  • NOT EXISTS — 반대로 결과가 없는 경우에 TRUE를 반환한다.

6. 활용 Plus — 리액트 추천 시스템 연동

리액트 스트리밍 앱의 "내가 아직 안 본 콘텐츠 추천" 기능 API 예시이다.

def get_recommendations(user_id):
    # 사용자가 아직 시청하지 않은 콘텐츠 중 평점 상위 3개 추천
    cur.execute("""
        SELECT title, genre, rating
        FROM contents
        WHERE id NOT IN (
            SELECT content_id
            FROM watches
            WHERE user_id = ?
        )
        ORDER BY rating DESC
        LIMIT 3
    """, (user_id,))
    rows = cur.fetchall()
    return [{"title": r[0], "genre": r[1], "rating": r[2]} for r in rows]

recs = get_recommendations(user_id=2)
print("=== user_id=2 추천 콘텐츠 ===")
for r in recs:
    print(r)

코드 설명:

  • 6~9행: 서브쿼리로 user_id=2가 이미 시청한 content_id 목록을 구한다.
  • 5행: NOT IN — 그 목록에 없는 콘텐츠만 가져온다. 즉, 아직 안 본 콘텐츠이다.
  • 11행: LIMIT 3 — 평점 높은 순으로 3개만 추천한다.

7. 문제풀기

목차

  • 구문