4강: 집계 함수
3강에서 ORDER BY와 LIMIT로 결과를 정렬하고 상위 N건을 가져왔습니다. 이번에는 '전체 고객이 몇 명인지', '평균 주문 금액은 얼마인지'처럼 여러 행을 하나의 숫자로 요약하는 집계 함수를 배웁니다.
이미 알고 계신다면
COUNT, COUNT(DISTINCT), SUM, AVG, MIN, MAX를 이미 알고 있다면 5강: GROUP BY로 건너뛰세요.
개념: 집계 함수는 여러 행을 하나의 값으로 요약합니다.
COUNT
COUNT(*)는 결과의 전체 행 수를 셉니다. COUNT(칼럼명)은 해당 칼럼에서 NULL이 아닌 값의 수를 셉니다.
결과:
| total_customers |
|---|
| 52300 |
-- 생년월일 등록 여부에 따른 고객 수 비교
SELECT
COUNT(*) AS total_customers,
COUNT(birth_date) AS with_birth_date,
COUNT(*) - COUNT(birth_date) AS missing_birth_date
FROM customers;
결과:
| total_customers | with_birth_date | missing_birth_date |
|---|---|---|
| 52300 | 44507 | 7793 |
COUNT(DISTINCT) — 고유값 개수
COUNT(DISTINCT 칼럼)은 중복을 제거한 후 개수를 셉니다. "몇 종류가 있는지" 알고 싶을 때 사용합니다.
-- 주문한 적 있는 고유 고객 수
SELECT
COUNT(*) AS total_orders,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders;
결과:
| total_orders | unique_customers |
|---|---|
| 417803 | 29230 |
주문은 34,908건이지만, 실제로 주문한 고객은 4,985명입니다. 한 고객이 여러 번 주문했기 때문입니다.
SUM
SUM은 숫자 칼럼의 합계를 구합니다. NULL 값은 무시됩니다.
-- 완료된 주문의 총 매출
SELECT SUM(total_amount) AS total_revenue
FROM orders
WHERE status IN ('delivered', 'confirmed');
결과:
| total_revenue |
|---|
| 393749378848.0 |
-- 활성 고객이 보유한 총 포인트
SELECT SUM(point_balance) AS total_points_outstanding
FROM customers
WHERE is_active = 1;
결과:
| total_points_outstanding |
|---|
| 3840575170 |
AVG
AVG는 산술 평균을 반환하며, NULL 값은 제외하고 계산합니다.
-- 판매 중인 상품의 평균 가격과 평균 재고
SELECT
AVG(price) AS avg_price,
AVG(stock_qty) AS avg_stock
FROM products
WHERE is_active = 1;
결과:
| avg_price | avg_stock |
|---|---|
| 678774.8505747126 | 250.53793103448277 |
-- 취소/반품을 제외한 주문의 평균 금액
SELECT AVG(total_amount) AS avg_order_value
FROM orders
WHERE status NOT IN ('cancelled', 'returned');
결과:
| avg_order_value |
|---|
| 1034451.993859959 |
ROUND — 반올림
AVG의 결과는 소수점이 길게 나올 수 있습니다. ROUND(값, 자릿수)로 원하는 소수점 자리까지 반올림할 수 있습니다.
-- 리뷰 평균 평점을 소수 첫째 자리까지
SELECT
AVG(rating) AS avg_raw,
ROUND(AVG(rating), 1) AS avg_rounded
FROM reviews;
결과:
| avg_raw | avg_rounded |
|---|---|
| 3.903090491521336 | 3.9 |
| avg_price |
|---|
| 665405 |
정수 나눗셈 주의
SQLite에서 정수 ÷ 정수 = 정수입니다. 소수점이 잘립니다:
-- 문제: 리뷰 작성률을 구하려는데...
SELECT
COUNT(*) AS total_orders,
(SELECT COUNT(*) FROM reviews) AS total_reviews,
(SELECT COUNT(*) FROM reviews) / COUNT(*) AS review_rate
FROM orders;
| total_orders | total_reviews | review_rate |
|---|---|---|
| 34908 | 7945 | 0 |
7945 ÷ 34908 = 0.2275...이지만, 정수 나눗셈이라 0이 됩니다. 해결법:
-- 해결: 한쪽을 실수(1.0)로 곱하기
SELECT ROUND((SELECT COUNT(*) FROM reviews) * 1.0 / COUNT(*) * 100, 1) AS review_rate_pct
FROM orders;
| review_rate_pct |
|---|
| 22.8 |
MySQL/PostgreSQL에서는?
MySQL과 PostgreSQL은 정수 나눗셈에서도 소수점이 유지되므로 이 문제가 발생하지 않습니다. SQLite 특유의 주의점입니다.
MIN과 MAX
MIN과 MAX는 칼럼에서 가장 작은 값과 가장 큰 값을 찾습니다.
-- 판매 중인 상품의 최저/최고 가격
SELECT
MIN(price) AS cheapest,
MAX(price) AS most_expensive
FROM products
WHERE is_active = 1;
결과:
| cheapest | most_expensive |
|---|---|
| 16500.0 | 7495200.0 |
-- 첫 주문일과 최근 주문일
SELECT
MIN(ordered_at) AS first_order,
MAX(ordered_at) AS latest_order
FROM orders;
결과:
| first_order | latest_order |
|---|---|
| 2016-01-02 13:54:14 | 2026-01-01 08:40:57 |
여러 집계 함수 동시 사용
하나의 SELECT에 여러 집계 함수를 함께 쓸 수 있습니다.
-- TechShop 리뷰 통계 요약
SELECT
COUNT(*) AS total_reviews,
AVG(rating) AS avg_rating,
MIN(rating) AS lowest_rating,
MAX(rating) AS highest_rating,
SUM(CASE WHEN rating = 5 THEN 1 ELSE 0 END) AS five_star_count
FROM reviews;
결과:
| total_reviews | avg_rating | lowest_rating | highest_rating | five_star_count |
|---|---|---|---|---|
| 95357 | 3.903090491521336 | 1 | 5 | 38460 |
집계 함수와 NULL
집계 함수는 NULL을 무시합니다. 이것은 중요한 동작입니다:
-- birth_date가 NULL인 고객이 약 15%
SELECT
COUNT(*) AS total,
COUNT(birth_date) AS with_birth,
AVG(CASE
WHEN birth_date IS NOT NULL
THEN 2025 - CAST(SUBSTR(birth_date, 1, 4) AS INTEGER)
END) AS avg_age
FROM customers;
| total | with_birth | avg_age |
|---|---|---|
| 5230 | 4450 | 39.2 |
COUNT(*)= 5,230 (NULL 포함 전체)COUNT(birth_date)= 4,450 (NULL 제외)AVG= 4,450명의 평균만 계산 (NULL인 780명은 제외)
NULL이 결과를 왜곡할 수 있습니다
AVG(birth_date 기반 나이)는 생년월일을 입력한 사람만의 평균입니다. 전체 고객의 평균 나이와는 다를 수 있습니다. NULL이 많은 칼럼을 집계할 때는 항상 COUNT(*)와 COUNT(칼럼)을 비교하여 NULL 비율을 확인하세요.
정리
| 함수 | 설명 | 예시 |
|---|---|---|
레슨 복습 문제
이 레슨에서 배운 개념을 바로 확인하는 간단한 문제입니다. 여러 개념을 종합하는 실전 연습은 연습 문제 섹션을 참고하세요.
문제 1
reviews 테이블에서 리뷰의 평균 평점을 소수점 2자리로 반올림하여 구하세요. 별칭은 avg_rating으로 지정하세요.
문제 2
orders 테이블에서 완료된 주문(status가 'delivered' 또는 'confirmed')의 총 매출(total_amount 합계)을 구하세요. 별칭은 total_revenue로 지정하세요.
정답
실행 결과 (1행)
| total_revenue |
|---|
| 34,582,358,414.00 |
문제 3
customers 테이블에서 전체 고객 수와 생년월일(birth_date)이 등록된 고객 수를 각각 구하세요. 별칭은 total_customers, with_birth_date로 지정하세요.
정답
실행 결과 (1행)
| total_customers | with_birth_date |
|---|---|
| 5230 | 4492 |
문제 4
TechShop에서 현재 판매 중인 상품 수를 세고, 해당 상품들의 총 재고 가치(price * stock_qty 합계)를 구하세요.
정답
SELECT
COUNT(*) AS active_product_count,
SUM(price * stock_qty) AS total_inventory_value
FROM products
WHERE is_active = 1;
실행 결과 (1행)
| active_product_count | total_inventory_value |
|---|---|
| 219 | 39,328,836,500.00 |
문제 5
취소 또는 반품되지 않은 주문의 total_amount 평균, 최솟값, 최댓값을 계산하세요. 별칭은 각각 avg_order, min_order, max_order로 지정하세요.
정답
SELECT
AVG(total_amount) AS avg_order,
MIN(total_amount) AS min_order,
MAX(total_amount) AS max_order
FROM orders
WHERE status NOT IN ('cancelled', 'returned', 'return_requested');
실행 결과 (1행)
| avg_order | min_order | max_order |
|---|---|---|
| 1,002,058.56 | 16,876.00 | 50,867,500.00 |
문제 6
products 테이블에서 판매 중인 상품의 평균 가격(avg_price, 소수점 0자리), 평균 원가(avg_cost, 소수점 0자리), 평균 마진율(avg_margin_pct, 소수점 1자리)을 한 쿼리로 구하세요. 마진율 = (price - cost_price) / price * 100으로 계산하되, 각 상품의 마진율의 평균을 구하세요.
정답
SELECT
ROUND(AVG(price), 0) AS avg_price,
ROUND(AVG(cost_price), 0) AS avg_cost,
ROUND(AVG((price - cost_price) / price * 100), 1) AS avg_margin_pct
FROM products
WHERE is_active = 1;
실행 결과 (1행)
| avg_price | avg_cost | avg_margin_pct |
|---|---|---|
| 656,583.00 | 502,003.00 | 24.00 |
문제 7
products 테이블에서 판매 중인 상품(is_active = 1)의 최저 가격, 최고 가격, 가격 차이를 구하세요. 별칭은 각각 min_price, max_price, price_range로 지정하세요.
정답
SELECT
MIN(price) AS min_price,
MAX(price) AS max_price,
MAX(price) - MIN(price) AS price_range
FROM products
WHERE is_active = 1;
실행 결과 (1행)
| min_price | max_price | price_range |
|---|---|---|
| 100.00 | 5,481,100.00 | 5,481,000.00 |
문제 8
order_items 테이블의 전체 행 수, 총 수량 합계(quantity), 평균 단가(unit_price, 소수점 2자리), 최대 수량을 구하세요. 별칭은 total_items, total_qty, avg_unit_price, max_qty로 지정하세요.
정답
SELECT
COUNT(*) AS total_items,
SUM(quantity) AS total_qty,
ROUND(AVG(unit_price), 2) AS avg_unit_price,
MAX(quantity) AS max_qty
FROM order_items;
실행 결과 (1행)
| total_items | total_qty | avg_unit_price | max_qty |
|---|---|---|---|
| 91,104 | 101,162 | 394,885.14 | 10 |
문제 9
payments 테이블에서 완료된 결제(status = 'completed')의 건수, 총 금액, 평균 금액(소수점 0자리), 최소/최대 금액을 모두 한 쿼리로 구하세요. 별칭은 payment_count, total_amount, avg_amount, min_amount, max_amount로 지정하세요.
정답
SELECT
COUNT(*) AS payment_count,
SUM(amount) AS total_amount,
ROUND(AVG(amount), 0) AS avg_amount,
MIN(amount) AS min_amount,
MAX(amount) AS max_amount
FROM payments
WHERE status = 'completed';
실행 결과 (1행)
| payment_count | total_amount | avg_amount | min_amount | max_amount |
|---|---|---|---|---|
| 34,616 | 34,682,197,764.00 | 1,001,912.00 | 16,876.00 | 50,867,500.00 |
문제 10
배송 메모(notes)가 있는 주문은 몇 건인지, 전체 주문 중 몇 퍼센트인지 구하세요. orders_with_notes, total_orders, pct_with_notes(소수점 1자리)를 반환하세요.