포스트

데이터를 집계하고 분석하는 SQL의 강력한 기능을 배워봐요! 📊

COUNT, SUM, AVG 같은 집계함수와 GROUP BY를 활용하면 데이터베이스에서 바로 통계를 계산할 수 있어요. Python으로 일일이 계산할 필요 없이 SQL 한 줄이면 끝! 실무에서는 판매 통계, 사용자 분석, 로그 집계 등에 필수적으로 사용되는 기능이에요.

(30분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: 집계함수 이해하기 (COUNT, SUM, AVG, MIN, MAX)

한 줄 설명

집계함수 = 여러 행을 계산하여 하나의 결과로 반환하는 함수

데이터를 세고(COUNT), 합치고(SUM), 평균내는(AVG) 등의 통계 작업을 SQL에서 바로 처리할 수 있어요!

기본 집계함수 5가지

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
import sqlite3

# 데이터베이스 연결
conn = sqlite3.connect('store.db')
cursor = conn.cursor()

# 샘플 데이터 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS sales (
    id INTEGER PRIMARY KEY,
    product TEXT,
    category TEXT,
    price INTEGER,
    quantity INTEGER,
    sale_date TEXT
)
''')

# 샘플 데이터 삽입
sales_data = [
    ('노트북', '전자제품', 1200000, 2, '2025-01-10'),
    ('키보드', '전자제품', 80000, 5, '2025-01-11'),
    ('마우스', '전자제품', 30000, 10, '2025-01-12'),
    ('책상', '가구', 150000, 3, '2025-01-13'),
    ('의자', '가구', 200000, 2, '2025-01-14'),
    ('모니터', '전자제품', 300000, 4, '2025-01-15'),
]

cursor.executemany(
    'INSERT INTO sales (product, category, price, quantity, sale_date) VALUES (?, ?, ?, ?, ?)',
    sales_data
)
conn.commit()

1. COUNT() - 행 개수 세기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 전체 판매 건수
cursor.execute('SELECT COUNT(*) FROM sales')
print(f"총 판매 건수: {cursor.fetchone()[0]}")
# 출력: 총 판매 건수: 6건

# 특정 카테고리 판매 건수
cursor.execute("SELECT COUNT(*) FROM sales WHERE category = '전자제품'")
print(f"전자제품 판매 건수: {cursor.fetchone()[0]}")
# 출력: 전자제품 판매 건수: 4건

# DISTINCT로 고유 카테고리 개수
cursor.execute('SELECT COUNT(DISTINCT category) FROM sales')
print(f"카테고리 종류: {cursor.fetchone()[0]}")
# 출력: 카테고리 종류: 2개

2. SUM() - 합계 계산

1
2
3
4
5
6
7
8
9
10
# 전체 판매 금액
cursor.execute('SELECT SUM(price * quantity) FROM sales')
total_revenue = cursor.fetchone()[0]
print(f"총 매출: {total_revenue:,}")
# 출력: 총 매출: 4,450,000원

# 전자제품 판매 수량 합계
cursor.execute("SELECT SUM(quantity) FROM sales WHERE category = '전자제품'")
print(f"전자제품 판매량: {cursor.fetchone()[0]}")
# 출력: 전자제품 판매량: 21개

3. AVG() - 평균 계산

1
2
3
4
5
6
7
8
9
10
# 평균 가격
cursor.execute('SELECT AVG(price) FROM sales')
avg_price = cursor.fetchone()[0]
print(f"평균 가격: {avg_price:,.0f}")
# 출력: 평균 가격: 326,667원

# 전자제품 평균 가격
cursor.execute("SELECT AVG(price) FROM sales WHERE category = '전자제품'")
print(f"전자제품 평균 가격: {cursor.fetchone()[0]:,.0f}")
# 출력: 전자제품 평균 가격: 402,500원

4. MIN() / MAX() - 최소값 / 최대값

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 최저가 상품
cursor.execute('SELECT MIN(price), product FROM sales')
min_price, product = cursor.fetchone()
print(f"최저가 상품: {product} ({min_price:,}원)")
# 출력: 최저가 상품: 마우스 (30,000원)

# 최고가 상품
cursor.execute('SELECT MAX(price), product FROM sales')
max_price, product = cursor.fetchone()
print(f"최고가 상품: {product} ({max_price:,}원)")
# 출력: 최고가 상품: 노트북 (1,200,000원)

# 가격 범위
cursor.execute('SELECT MIN(price) AS 최저가, MAX(price) AS 최고가 FROM sales')
result = cursor.fetchone()
print(f"가격 범위: {result[0]:,}원 ~ {result[1]:,}")
# 출력: 가격 범위: 30,000원 ~ 1,200,000원

여러 집계함수 동시 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 한 번에 여러 통계 조회
cursor.execute('''
    SELECT
        COUNT(*) AS 총건수,
        SUM(price * quantity) AS 총매출,
        AVG(price) AS 평균가격,
        MIN(price) AS 최저가,
        MAX(price) AS 최고가
    FROM sales
''')

stats = cursor.fetchone()
print(f"""
=== 판매 통계 ===
총 건수: {stats[0]}건
총 매출: {stats[1]:,}원
평균 가격: {stats[2]:,.0f}원
최저가: {stats[3]:,}원
최고가: {stats[4]:,}""")

출력:

1
2
3
4
5
6
=== 판매 통계 ===
총 건수: 6건
총 매출: 4,450,000원
평균 가격: 326,667원
최저가: 30,000원
최고가: 1,200,000원

🎯 학습 목표 2: GROUP BY로 그룹화하기

한 줄 설명

GROUP BY = 같은 값을 가진 행들을 그룹으로 묶어 집계

카테고리별, 날짜별, 사용자별 등 특정 기준으로 데이터를 그룹화해서 통계를 낼 수 있어요!

기본 GROUP BY 사용법

1
2
3
4
5
6
7
8
9
10
# 카테고리별 판매 건수
cursor.execute('''
    SELECT category, COUNT(*) AS 판매건수
    FROM sales
    GROUP BY category
''')

print("=== 카테고리별 판매 건수 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]}")

출력:

1
2
3
=== 카테고리별 판매 건수 ===
가구: 2건
전자제품: 4건

카테고리별 매출 통계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cursor.execute('''
    SELECT
        category,
        COUNT(*) AS 판매건수,
        SUM(price * quantity) AS 총매출,
        AVG(price) AS 평균가격,
        SUM(quantity) AS 판매량
    FROM sales
    GROUP BY category
    ORDER BY 총매출 DESC
''')

print("\n=== 카테고리별 상세 통계 ===")
for row in cursor.fetchall():
    print(f"""
카테고리: {row[0]}
  판매 건수: {row[1]}건
  총 매출: {row[2]:,}원
  평균 가격: {row[3]:,.0f}원
  판매량: {row[4]}""")

출력:

1
2
3
4
5
6
7
8
9
10
11
12
=== 카테고리별 상세 통계 ===
카테고리: 전자제품
  판매 건수: 4건
  총 매출: 3,300,000원
  평균 가격: 402,500원
  판매량: 21개

카테고리: 가구
  판매 건수: 2건
  총 매출: 850,000원
  평균 가격: 175,000원
  판매량: 5개

날짜별 매출 추이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 날짜별 매출
cursor.execute('''
    SELECT
        sale_date,
        COUNT(*) AS 판매건수,
        SUM(price * quantity) AS 일매출
    FROM sales
    GROUP BY sale_date
    ORDER BY sale_date
''')

print("\n=== 일별 매출 추이 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]}건, {row[2]:,}")

출력:

1
2
3
4
5
6
7
=== 일별 매출 추이 ===
2025-01-10: 1건, 2,400,000원
2025-01-11: 1건, 400,000원
2025-01-12: 1건, 300,000원
2025-01-13: 1건, 450,000원
2025-01-14: 1건, 400,000원
2025-01-15: 1건, 1,200,000원

가격대별 상품 분류

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cursor.execute('''
    SELECT
        CASE
            WHEN price < 100000 THEN '저가'
            WHEN price < 500000 THEN '중가'
            ELSE '고가'
        END AS 가격대,
        COUNT(*) AS 상품수,
        AVG(price) AS 평균가격
    FROM sales
    GROUP BY 가격대
    ORDER BY 평균가격
''')

print("\n=== 가격대별 상품 분포 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]}개 (평균 {row[2]:,.0f}원)")

출력:

1
2
3
4
=== 가격대별 상품 분포 ===
저가: 2개 (평균 55,000원)
중가: 2개 (평균 250,000원)
고가: 2개 (평균 750,000원)

🎯 학습 목표 3: HAVING으로 그룹 조건 걸기

한 줄 설명

HAVING = GROUP BY 결과에 조건을 거는 절

WHERE는 그룹화 전 행을 필터링하고, HAVING은 그룹화 후 결과를 필터링해요!

WHERE vs HAVING 차이

1
2
3
4
5
6
7
8
9
10
11
# WHERE: 그룹화 전 필터링 (개별 행)
cursor.execute('''
    SELECT category, COUNT(*) AS 판매건수
    FROM sales
    WHERE price >= 100000  -- 10만원 이상 상품만
    GROUP BY category
''')

print("=== WHERE: 10만원 이상 상품만 집계 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]}")

출력:

1
2
3
=== WHERE: 10만원 이상 상품만 집계 ===
가구: 2건
전자제품: 2건
1
2
3
4
5
6
7
8
9
10
11
# HAVING: 그룹화 후 필터링 (그룹 결과)
cursor.execute('''
    SELECT category, COUNT(*) AS 판매건수
    FROM sales
    GROUP BY category
    HAVING COUNT(*) >= 3  -- 판매건수 3건 이상인 카테고리만
''')

print("\n=== HAVING: 판매건수 3건 이상 카테고리 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]}")

출력:

1
2
=== HAVING: 판매건수 3건 이상 카테고리 ===
전자제품: 4건

HAVING으로 상위 그룹 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 매출 100만원 이상 카테고리
cursor.execute('''
    SELECT
        category,
        SUM(price * quantity) AS 총매출,
        COUNT(*) AS 판매건수
    FROM sales
    GROUP BY category
    HAVING SUM(price * quantity) >= 1000000
    ORDER BY 총매출 DESC
''')

print("\n=== 매출 100만원 이상 카테고리 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]:,}원 ({row[2]}건)")

출력:

1
2
=== 매출 100만원 이상 카테고리 ===
전자제품: 3,300,000원 (4건)

WHERE + HAVING 조합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# WHERE로 먼저 필터링 → GROUP BY → HAVING으로 다시 필터링
cursor.execute('''
    SELECT
        category,
        AVG(price) AS 평균가격,
        COUNT(*) AS 상품수
    FROM sales
    WHERE sale_date >= '2025-01-12'  -- 1월 12일 이후
    GROUP BY category
    HAVING COUNT(*) >= 2  -- 2개 이상 판매된 카테고리만
''')

print("\n=== 1월 12일 이후 2개 이상 판매 카테고리 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: 평균 {row[1]:,.0f}원 ({row[2]}개)")

출력:

1
2
3
=== 1월 12일 이후 2개 이상 판매 카테고리 ===
전자제품: 평균 165,000원 (2개)
가구: 평균 175,000원 (2개)

🎯 학습 목표 4: 서브쿼리와 고급 기법 익히기

한 줄 설명

서브쿼리 = 쿼리 안에 포함된 또 다른 쿼리

복잡한 조건이나 계산을 단계적으로 처리할 수 있어요!

서브쿼리 기본

1
2
3
4
5
6
7
8
9
10
11
# 평균 가격보다 비싼 상품 찾기
cursor.execute('''
    SELECT product, price
    FROM sales
    WHERE price > (SELECT AVG(price) FROM sales)
    ORDER BY price DESC
''')

print("=== 평균가보다 비싼 상품 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]:,}")

출력:

1
2
3
=== 평균가보다 비싼 상품 ===
노트북: 1,200,000원
모니터: 300,000원

IN을 사용한 서브쿼리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 가장 많이 팔린 카테고리의 상품들
cursor.execute('''
    SELECT product, category, quantity
    FROM sales
    WHERE category = (
        SELECT category
        FROM sales
        GROUP BY category
        ORDER BY SUM(quantity) DESC
        LIMIT 1
    )
''')

print("\n=== 판매량 1위 카테고리 상품들 ===")
for row in cursor.fetchall():
    print(f"{row[0]} ({row[1]}): {row[2]}")

출력:

1
2
3
4
5
=== 판매량 1위 카테고리 상품들 ===
노트북 (전자제품): 2개
키보드 (전자제품): 5개
마우스 (전자제품): 10개
모니터 (전자제품): 4개

DISTINCT로 중복 제거

1
2
3
4
5
6
# 판매된 카테고리 목록 (중복 제거)
cursor.execute('SELECT DISTINCT category FROM sales ORDER BY category')

print("\n=== 판매 카테고리 ===")
for row in cursor.fetchall():
    print(f"- {row[0]}")

출력:

1
2
3
=== 판매 카테고리 ===
- 가구
- 전자제품

실전 통계 쿼리 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 카테고리별 베스트셀러
cursor.execute('''
    SELECT
        category,
        product,
        price * quantity AS 매출
    FROM sales s1
    WHERE price * quantity = (
        SELECT MAX(price * quantity)
        FROM sales s2
        WHERE s1.category = s2.category
    )
    ORDER BY 매출 DESC
''')

print("\n=== 카테고리별 베스트셀러 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]} ({row[2]:,}원)")

출력:

1
2
3
=== 카테고리별 베스트셀러 ===
전자제품: 노트북 (2,400,000원)
가구: 책상 (450,000원)

💡 실전 팁 & 주의사항

💡 Tip 1: GROUP BY 시 SELECT 절 제한

1
2
3
4
5
6
7
8
9
10
11
# ❌ 잘못된 예 - GROUP BY에 없는 컬럼은 SELECT 불가
# SELECT product, category, COUNT(*)
# FROM sales
# GROUP BY category

# ✅ 올바른 예 - GROUP BY의 컬럼이나 집계함수만 SELECT
cursor.execute('''
    SELECT category, COUNT(*) AS 개수
    FROM sales
    GROUP BY category
''')

💡 Tip 2: HAVING은 항상 GROUP BY 뒤에

1
2
3
4
5
6
7
8
9
# ✅ 올바른 순서
cursor.execute('''
    SELECT category, COUNT(*)
    FROM sales
    WHERE price > 50000       -- 1. WHERE로 행 필터링
    GROUP BY category         -- 2. 그룹화
    HAVING COUNT(*) >= 2      -- 3. HAVING으로 그룹 필터링
    ORDER BY COUNT(*) DESC    -- 4. 정렬
''')

💡 Tip 3: NULL 값 처리

1
2
3
4
5
6
7
8
# COUNT(*)는 NULL 포함, COUNT(column)은 NULL 제외
cursor.execute('''
    SELECT
        COUNT(*) AS 전체행수,
        COUNT(product) AS 상품수,
        COUNT(DISTINCT category) AS 카테고리수
    FROM sales
''')

💡 Tip 4: AS 별칭 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 별칭으로 가독성 향상
cursor.execute('''
    SELECT
        category AS 카테고리,
        COUNT(*) AS 판매건수,
        SUM(price * quantity) AS 총매출,
        AVG(price) AS 평균가격
    FROM sales
    GROUP BY category
''')

# fetchone() 결과를 딕셔너리처럼 사용 (별칭 활용)
for row in cursor.fetchall():
    print(f"{row[0]}: 매출 {row[2]:,}")

🧪 연습 문제

문제 1: 월별 매출 분석

샘플 데이터를 확장하여 여러 달의 판매 데이터를 분석하세요.

요구사항:

  1. 월별 총 매출을 계산하세요
  2. 월별 평균 주문 금액을 계산하세요
  3. 매출이 200만원 이상인 월만 출력하세요
💡 힌트💡 힌트
  • substr(sale_date, 1, 7)로 ‘YYYY-MM’ 추출
  • GROUP BY 월별로 그룹화
  • HAVING으로 매출 조건 필터링
</details>
✅ 정답
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
# 여러 달 데이터 추가
additional_sales = [
    ('스마트폰', '전자제품', 900000, 3, '2025-02-10'),
    ('태블릿', '전자제품', 600000, 2, '2025-02-15'),
    ('침대', '가구', 800000, 1, '2025-02-20'),
    ('소파', '가구', 1200000, 1, '2025-03-05'),
    ('냉장고', '전자제품', 1500000, 2, '2025-03-10'),
]

cursor.executemany(
    'INSERT INTO sales (product, category, price, quantity, sale_date) VALUES (?, ?, ?, ?, ?)',
    additional_sales
)
conn.commit()

# 월별 매출 분석
cursor.execute('''
    SELECT
        substr(sale_date, 1, 7) AS 월,
        COUNT(*) AS 판매건수,
        SUM(price * quantity) AS 총매출,
        AVG(price * quantity) AS 평균주문금액
    FROM sales
    GROUP BY 월
    HAVING 총매출 >= 2000000
    ORDER BY 월
''')

print("=== 월별 매출 분석 (200만원 이상) ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]}건, 총매출 {row[2]:,}원, 평균 {row[3]:,.0f}")

출력:

1
2
3
4
=== 월별 매출 분석 (200만원 이상) ===
2025-01: 6건, 총매출 4,450,000원, 평균 741,667원
2025-02: 3건, 총매출 3,500,000원, 평균 1,166,667원
2025-03: 2건, 총매출 4,200,000원, 평균 2,100,000원
--- ### 문제 2: 카테고리별 베스트 상품 **과제**: 각 카테고리에서 가장 많이 팔린 상품을 찾으세요. **요구사항**: 1. 카테고리별로 상품별 판매량 합계를 계산하세요 2. 각 카테고리에서 판매량이 가장 많은 상품만 출력하세요 3. 서브쿼리를 활용하세요
💡 힌트
  • GROUP BY category, product
  • SUM(quantity)로 판매량 합계
  • WHERE절에 서브쿼리로 MAX 비교
✅ 정답
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
# 카테고리별 상품별 판매량
cursor.execute('''
    SELECT
        category,
        product,
        SUM(quantity) AS 총판매량,
        SUM(price * quantity) AS 총매출
    FROM sales
    GROUP BY category, product
    ORDER BY category, 총판매량 DESC
''')

print("\n=== 카테고리별 상품별 판매량 ===")
for row in cursor.fetchall():
    print(f"{row[0]} - {row[1]}: {row[2]}개 (매출 {row[3]:,}원)")

# 카테고리별 베스트셀러 (서브쿼리 사용)
cursor.execute('''
    SELECT
        s1.category,
        s1.product,
        SUM(s1.quantity) AS 총판매량
    FROM sales s1
    GROUP BY s1.category, s1.product
    HAVING SUM(s1.quantity) = (
        SELECT MAX(total_qty)
        FROM (
            SELECT SUM(quantity) AS total_qty
            FROM sales s2
            WHERE s2.category = s1.category
            GROUP BY s2.product
        )
    )
    ORDER BY s1.category
''')

print("\n=== 카테고리별 베스트셀러 ===")
for row in cursor.fetchall():
    print(f"{row[0]}: {row[1]} ({row[2]}개)")

출력:

1
2
3
4
5
6
7
8
9
10
11
=== 카테고리별 상품별 판매량 ===
가구 - 책상: 3개 (매출 450,000원)
가구 - 의자: 2개 (매출 400,000원)
가구 - 침대: 1개 (매출 800,000원)
전자제품 - 마우스: 10개 (매출 300,000원)
전자제품 - 키보드: 5개 (매출 400,000원)
전자제품 - 모니터: 4개 (매출 1,200,000원)

=== 카테고리별 베스트셀러 ===
가구: 책상 (3개)
전자제품: 마우스 (10개)
--- ## 📝 오늘 배운 내용 정리 1. **집계함수**: COUNT, SUM, AVG, MIN, MAX로 통계 계산 2. **GROUP BY**: 특정 컬럼 기준으로 그룹화 3. **HAVING**: 그룹화 결과에 조건 적용 4. **DISTINCT**: 중복 제거 5. **서브쿼리**: 쿼리 안에 쿼리 작성 6. **실전 활용**: 매출 분석, 베스트셀러 찾기, 통계 리포트 --- ## 🔗 관련 자료 - [SQL 집계함수 (공식 문서)](https://www.sqlite.org/lang_aggfunc.html) - [GROUP BY 절 (SQLite)](https://www.sqlite.org/lang_select.html#resultset) - [서브쿼리 가이드](https://www.sqlitetutorial.net/sqlite-subquery/) --- ## 📚 이전 학습 **Day 76**: [SQL 고급 - JOIN](/posts/python-100days-day76-sql-join) ⭐⭐⭐ 어제는 여러 테이블을 연결하는 JOIN을 배웠어요! ## 📚 다음 학습 **Day 78**: [Python과 SQLite 연동 심화](/posts/python-100days-day78-python-sqlite) ⭐⭐⭐ 내일은 Python에서 안전하고 효율적으로 데이터베이스를 다루는 고급 기법을 배워요! --- > "데이터를 집계하고 분석하는 능력은 데이터 과학의 시작입니다!" 📊 > > **Day 77/100** | Phase 8: 데이터베이스와 SQL | #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.