SQL 고급 문법 - GROUP BY, JOIN, 서브쿼리 완전 가이드
SQL 고급 문법 - GROUP BY, JOIN, 서브쿼리 완전 가이드
SQL 고급 문법 - GROUP BY, JOIN, 서브쿼리 완전 가이드
개요
SQL의 고급 문법을 통해 복잡한 데이터 분석과 조작이 가능합니다:
- GROUP BY: 데이터 그룹화 및 집계
- JOIN: 여러 테이블 연결
- 서브쿼리: 중첩된 쿼리 구조
- 조건문: IF, CASE WHEN 활용
- 날짜 함수: DATE_FORMAT 등
1. GROUP BY - 데이터 그룹화
기본 GROUP BY
1
2
3
4
5
6
7
8
9
-- 국가코드별 평균 인구수
SELECT CountryCode, AVG(Population) AS "avg_pop"
FROM city
GROUP BY CountryCode;
-- 여러 컬럼으로 그룹화
SELECT COUNTRYCODE, DISTRICT, COUNT(*)
FROM city
GROUP BY COUNTRYCODE, DISTRICT;
집계 함수와 GROUP BY
1
2
3
4
5
6
7
8
9
-- 국가코드별 다양한 집계 통계
SELECT COUNTRYCODE,
AVG(POPULATION) AS "AVG_POP",
STDDEV(POPULATION) AS "STD_POP",
MAX(POPULATION) AS "MAX_POP",
MIN(POPULATION) AS "MIN_POP"
FROM city
GROUP BY COUNTRYCODE
ORDER BY STD_POP DESC;
WHERE와 HAVING의 차이
1
2
3
4
5
6
7
8
9
10
11
12
-- WHERE: 그룹화 전 필터링
-- HAVING: 그룹화 후 필터링
SELECT COUNTRYCODE,
AVG(POPULATION) AS "AVG_POP",
STDDEV(POPULATION) AS "STD_POP",
MAX(POPULATION) AS "MAX_POP",
MIN(POPULATION) AS "MIN_POP"
FROM city
WHERE POPULATION >= 5000000 -- 그룹화 전 필터링
GROUP BY COUNTRYCODE
HAVING STD_POP >= 300000 -- 그룹화 후 필터링
ORDER BY STD_POP DESC;
복합 조건 GROUP BY
1
2
3
4
5
6
-- 나라코드에 K가 들어가는 나라들의 인구수 총합이 5백만 이상인 나라만 조회
SELECT COUNTRYCODE, SUM(POPULATION) 'SUM_POP'
FROM city
WHERE COUNTRYCODE LIKE '%K%'
GROUP BY COUNTRYCODE
HAVING SUM_POP >= 5000000;
실무 예제 - 대륙별 분석
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. 대륙별 인구수와 GNP 최대값
SELECT Continent, SUM(population), MAX(gnp)
FROM country
GROUP BY Continent;
-- 2. 대륙별 인구수와 GNP 평균 (GNP와 인구수가 0이 아닌 데이터)
SELECT Continent, SUM(population), AVG(gnp)
FROM country
WHERE GNP != 0 AND POPULATION != 0
GROUP BY Continent;
-- 3. 대륙별 평균 인구수와 평균 GNP (인구수로 내림차순 정렬)
SELECT Continent, AVG(population) AVG_POP, AVG(gnp)
FROM country
WHERE GNP != 0 AND POPULATION != 0
GROUP BY Continent
ORDER BY AVG_POP DESC;
-- 4. 대륙별 1인당 GNP 계산
SELECT Continent,
AVG(population) AVG_POP,
AVG(gnp) avg_gnp,
AVG(gnp)/AVG(population)*1000 GNP_POP_AVG
FROM country
WHERE GNP != 0 AND POPULATION != 0
GROUP BY Continent
HAVING GNP_POP_AVG >= 0.01
ORDER BY GNP_POP_AVG DESC;
2. SELECT 절 고급 문법
수학 함수
CEIL (올림)
1
2
3
-- 실수 데이터에서 올림
SELECT CEIL(12.345); -- 결과: 13
SELECT CEIL(-12.345); -- 결과: -12
ROUND (반올림)
1
2
3
4
5
6
7
-- 반올림 (소수점 둘째 자리까지)
SELECT ROUND(12.345, 2), ROUND(12.343, 2);
-- 결과: 12.35, 12.34
-- 국가별 언어 사용 비율을 소수 첫 번째 자리에서 반올림하여 정수로 표현
SELECT COUNTRYCODE, LANGUAGE, ISOFFICIAL, ROUND(PERCENTAGE, 0)
FROM countrylanguage;
TRUNCATE (버림)
1
2
3
-- 실수 데이터를 버림(내림)
SELECT TRUNCATE(12.345, 2); -- 결과: 12.34
SELECT TRUNCATE(12.345, 1); -- 결과: 12.3
조건문
IF 함수
1
2
3
4
5
6
7
8
-- 기본 IF 문법: IF(조건, 참일 때 값, 거짓일 때 값)
SELECT IF(10 > 11, "참입니다", "거짓입니다");
-- 결과: "거짓입니다"
-- 실무 예제: 인구수에 따른 도시 규모 분류
SELECT Name, Population,
IF(Population >= 1000000, "대도시", "소도시") AS city_type
FROM city;
CASE WHEN (고급 조건문)
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
-- CASE WHEN 기본 문법
CASE
WHEN (조건1) THEN expr
WHEN (조건2) THEN expr
WHEN (조건3) THEN expr
ELSE expr
END
-- 도시 규모 분류 (다단계 조건)
SELECT Name, Population,
CASE
WHEN POPULATION >= 1000000 THEN "Big City"
WHEN POPULATION BETWEEN 500000 AND 999999 THEN "Middle City"
ELSE "Small City"
END CITY_SCALE
FROM city;
-- 등급 분류 예제
SELECT Name, Population,
CASE
WHEN Population >= 10000000 THEN "A급"
WHEN Population >= 5000000 THEN "B급"
WHEN Population >= 1000000 THEN "C급"
WHEN Population >= 500000 THEN "D급"
ELSE "E급"
END city_grade
FROM city
ORDER BY Population DESC;
날짜 함수
DATE_FORMAT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 날짜 데이터 포맷 변경
USE sakila;
SELECT payment_date,
DATE_FORMAT(payment_date, "%Y") as "year",
DATE_FORMAT(payment_date, "%m") as "month",
DATE_FORMAT(payment_date, "%d") as "day",
DATE_FORMAT(payment_date, "%Y년 %m월 %d일") as "formatted_date"
FROM payment;
-- 다양한 날짜 포맷
SELECT
payment_date,
DATE_FORMAT(payment_date, "%Y-%m-%d") as "date_only",
DATE_FORMAT(payment_date, "%H:%i:%s") as "time_only",
DATE_FORMAT(payment_date, "%Y년 %m월 %d일 %H시 %i분") as "korean_format"
FROM payment;
날짜 관련 함수들
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 현재 날짜/시간
SELECT NOW(), CURDATE(), CURTIME();
-- 날짜 계산
SELECT
payment_date,
DATE_ADD(payment_date, INTERVAL 1 DAY) as "next_day",
DATE_SUB(payment_date, INTERVAL 1 MONTH) as "last_month",
DATEDIFF(NOW(), payment_date) as "days_ago"
FROM payment;
-- 날짜 추출
SELECT
payment_date,
YEAR(payment_date) as "year",
MONTH(payment_date) as "month",
DAY(payment_date) as "day",
DAYOFWEEK(payment_date) as "day_of_week"
FROM payment;
3. JOIN - 테이블 연결
Cartesian Join (카테시안 조인)
1
2
3
4
5
-- Cartesian Join (교차곱)
-- 클라이언트 서비스에서는 사용하지 않음
-- 데이터 분석에서 집계용 코드 부여 시 사용
SELECT * FROM user, addr;
-- 결과: user 테이블의 모든 행 × addr 테이블의 모든 행
INNER JOIN
1
2
3
4
5
6
7
8
9
-- INNER JOIN: 두 테이블의 공통 데이터만 조회
SELECT *
FROM user -- FROM절에 오는 테이블이 기준 테이블
JOIN addr ON user.user_id = addr.user_id; -- 대상 테이블
-- 명시적 INNER JOIN
SELECT u.name, u.age, a.addr
FROM user u
INNER JOIN addr a ON u.user_id = a.user_id;
LEFT JOIN
1
2
3
4
5
6
7
8
9
10
11
12
13
-- LEFT JOIN: 왼쪽 테이블의 모든 데이터 + 오른쪽 테이블의 매칭 데이터
-- 모든 사용자의 이름과 주소를 출력 (주소가 없는 사람은 "주소없음"으로 출력)
SELECT user.name, IFNULL(addr.addr, "주소없음") as address
FROM user
LEFT JOIN addr ON user.user_id = addr.user_id;
-- 실무 예제: 고객별 주문 내역 (주문이 없는 고객도 포함)
SELECT c.customer_name,
IFNULL(COUNT(o.order_id), 0) as order_count,
IFNULL(SUM(o.amount), 0) as total_amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name;
RIGHT JOIN
1
2
3
4
-- RIGHT JOIN: 오른쪽 테이블의 모든 데이터 + 왼쪽 테이블의 매칭 데이터
SELECT u.name, a.addr
FROM user u
RIGHT JOIN addr a ON u.user_id = a.user_id;
FULL OUTER JOIN
1
2
3
4
5
6
7
8
9
-- FULL OUTER JOIN: 양쪽 테이블의 모든 데이터
-- MySQL에서는 지원하지 않음 (UNION으로 구현)
SELECT u.name, a.addr
FROM user u
LEFT JOIN addr a ON u.user_id = a.user_id
UNION
SELECT u.name, a.addr
FROM user u
RIGHT JOIN addr a ON u.user_id = a.user_id;
SELF JOIN
1
2
3
4
5
-- SELF JOIN: 같은 테이블을 조인
-- 직원과 상사의 관계 조회
SELECT e.employee_name, m.employee_name as manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
4. 서브쿼리 (Subquery)
SELECT 절 서브쿼리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- SELECT 절에서 서브쿼리 사용
-- 도시 이름에 k가 들어가는 도시의 개수, 나라 이름에 A가 들어가는 나라들의 GNP 평균
USE world;
SELECT
(SELECT COUNT(*) FROM city WHERE name LIKE '%K%') as city_count,
(SELECT AVG(gnp) FROM country WHERE name LIKE '%A%') as avg_gnp;
-- 상관 서브쿼리 예제
SELECT
c.name as country_name,
(SELECT COUNT(*) FROM city WHERE CountryCode = c.Code) as city_count,
(SELECT AVG(Population) FROM city WHERE CountryCode = c.Code) as avg_city_population
FROM country c;
FROM 절 서브쿼리
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
-- FROM 절에서 서브쿼리 사용 (인라인 뷰)
SELECT city_sub.CountryCode,
city_sub.Name as "city_name",
city_sub.Population as "city_pop",
country.Name as "country_name"
FROM (
SELECT COUNTRYCODE, NAME, POPULATION
FROM city
WHERE POPULATION >= 8000000
) AS city_sub
JOIN country ON city_sub.CountryCode = country.Code;
-- 복잡한 집계 후 조인
SELECT
sales_summary.product_id,
p.product_name,
sales_summary.total_sales,
sales_summary.avg_price
FROM (
SELECT
product_id,
SUM(quantity) as total_sales,
AVG(price) as avg_price
FROM sales
WHERE sale_date >= '2023-01-01'
GROUP BY product_id
HAVING total_sales > 100
) AS sales_summary
JOIN products p ON sales_summary.product_id = p.product_id;
WHERE 절 서브쿼리
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
-- WHERE 절에서 서브쿼리 사용
-- 도시 인구수 800만 이상 도시의 국가코드, 국가 이름, 대통령 이름 출력
SELECT code, NAME, headofstate
FROM country
WHERE CODE IN (
SELECT DISTINCT(CountryCode)
FROM city
WHERE POPULATION >= 8000000
);
-- EXISTS 사용
SELECT c.name
FROM country c
WHERE EXISTS (
SELECT 1
FROM city
WHERE CountryCode = c.Code AND Population > 10000000
);
-- 비교 연산자와 서브쿼리
SELECT name, population
FROM country
WHERE population > (
SELECT AVG(population)
FROM country
WHERE continent = 'Asia'
);
서브쿼리 성능 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- IN vs EXISTS 성능 비교
-- IN: 서브쿼리 결과가 적을 때 유리
SELECT * FROM orders
WHERE customer_id IN (
SELECT customer_id FROM customers WHERE status = 'active'
);
-- EXISTS: 서브쿼리 결과가 많을 때 유리
SELECT * FROM orders o
WHERE EXISTS (
SELECT 1 FROM customers c
WHERE c.customer_id = o.customer_id AND c.status = 'active'
);
-- JOIN으로 변환 (가장 효율적)
SELECT o.* FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.status = 'active';
5. 고급 집계 함수
윈도우 함수 (Window Functions)
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
-- ROW_NUMBER: 순위 매기기
SELECT
name,
population,
ROW_NUMBER() OVER (ORDER BY population DESC) as rank
FROM city;
-- RANK: 동일한 값에 같은 순위 부여
SELECT
name,
population,
RANK() OVER (ORDER BY population DESC) as rank
FROM city;
-- DENSE_RANK: 동일한 값에 같은 순위, 다음 순위는 연속
SELECT
name,
population,
DENSE_RANK() OVER (ORDER BY population DESC) as rank
FROM city;
-- PARTITION BY: 그룹별 순위
SELECT
CountryCode,
name,
population,
ROW_NUMBER() OVER (PARTITION BY CountryCode ORDER BY population DESC) as country_rank
FROM city;
누적 집계
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 누적 합계
SELECT
name,
population,
SUM(population) OVER (ORDER BY population DESC) as cumulative_population
FROM city;
-- 이동 평균
SELECT
name,
population,
AVG(population) OVER (
ORDER BY population DESC
ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING
) as moving_avg
FROM city;
6. 실무 활용 예제
매출 분석 쿼리
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
-- 월별 매출 분석
SELECT
DATE_FORMAT(order_date, '%Y-%m') as month,
COUNT(*) as order_count,
SUM(amount) as total_sales,
AVG(amount) as avg_order_value,
MAX(amount) as max_order_value
FROM orders
WHERE order_date >= '2023-01-01'
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;
-- 고객별 구매 패턴 분석
SELECT
c.customer_name,
COUNT(o.order_id) as order_count,
SUM(o.amount) as total_spent,
AVG(o.amount) as avg_order_value,
MAX(o.order_date) as last_order_date,
DATEDIFF(NOW(), MAX(o.order_date)) as days_since_last_order
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
HAVING total_spent > 1000
ORDER BY total_spent DESC;
재고 관리 쿼리
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
-- 재고 부족 상품 조회
SELECT
p.product_name,
p.current_stock,
p.min_stock_level,
(p.min_stock_level - p.current_stock) as shortage_amount,
CASE
WHEN p.current_stock = 0 THEN '품절'
WHEN p.current_stock <= p.min_stock_level THEN '재고부족'
ELSE '정상'
END as stock_status
FROM products p
WHERE p.current_stock <= p.min_stock_level
ORDER BY shortage_amount DESC;
-- 카테고리별 재고 현황
SELECT
c.category_name,
COUNT(p.product_id) as product_count,
SUM(p.current_stock) as total_stock,
AVG(p.current_stock) as avg_stock,
SUM(CASE WHEN p.current_stock <= p.min_stock_level THEN 1 ELSE 0 END) as low_stock_count
FROM categories c
LEFT JOIN products p ON c.category_id = p.category_id
GROUP BY c.category_id, c.category_name
ORDER BY low_stock_count DESC;
7. 성능 최적화 팁
인덱스 활용
1
2
3
4
5
6
7
-- 자주 사용되는 컬럼에 인덱스 생성
CREATE INDEX idx_city_countrycode ON city(CountryCode);
CREATE INDEX idx_city_population ON city(Population);
CREATE INDEX idx_country_continent ON country(Continent);
-- 복합 인덱스
CREATE INDEX idx_city_country_pop ON city(CountryCode, Population);
쿼리 최적화
1
2
3
4
5
6
7
8
9
10
11
-- 비효율적인 쿼리
SELECT * FROM city WHERE UPPER(name) = 'SEOUL';
-- 효율적인 쿼리
SELECT * FROM city WHERE name = 'Seoul';
-- LIMIT 사용으로 결과 제한
SELECT * FROM city ORDER BY population DESC LIMIT 10;
-- 필요한 컬럼만 선택
SELECT name, population FROM city WHERE CountryCode = 'KOR';
마무리
SQL의 고급 문법을 통해 복잡한 데이터 분석과 조작이 가능합니다:
핵심 포인트
- GROUP BY: 데이터 그룹화와 집계 분석
- JOIN: 여러 테이블의 데이터 연결
- 서브쿼리: 중첩된 쿼리로 복잡한 조건 처리
- 조건문: IF, CASE WHEN으로 데이터 분류
- 날짜 함수: DATE_FORMAT으로 날짜 데이터 처리
실무 활용
- 매출 분석, 재고 관리, 고객 분석 등 다양한 비즈니스 요구사항 해결
- 성능 최적화를 통한 효율적인 데이터 처리
- 인덱스 활용과 쿼리 최적화로 시스템 성능 향상
이러한 고급 SQL 문법을 익혀 실무에서 데이터 분석과 비즈니스 인사이트 도출에 활용하세요.
This post is licensed under CC BY 4.0 by the author.