1. 고객분석 (Customers)
- 국가별 고객 수, 누적 합계
- 국가별 고객 수, 구성비, 누적비
- 주문이력이 없는 고객
1.1 국가 별 고객 수, 누적 합계
- 고객 수로 내림차순 정렬
- 고객 수가 동일한 경우 국가명으로 오름차순 정렬
- cte 구문 사용하지 않고 서브쿼리를 사용하여 작성
SELECT 국가명
, 고객수
, sum(고객수) over(ORDER BY 고객수 DESC, 국가명) AS 누적합계
FROM (SELECT country AS 국가명
, count(DISTINCT customer_id) AS 고객수
FROM customers
GROUP BY country) AS a
ORDER BY 고객수 desc, 국가명 ;
1.2 국가 별 고객 수, 구성비, 누적 비
- 고객 수로 내림차순 정렬
- 고객 수가 동일한 경우 국가명으로 오름차순 정렬
- cte 구문 사용하지 않고 서브쿼리를 사용하여 작성
SELECT *
, sum(구성비) over(ORDER BY 구성비 DESC, 국가명)
FROM (
-- 구성비
SELECT *
, 고객수 / sum(고객수) over() AS 구성비
FROM (
-- 고객수
SELECT country AS 국가명
, count(DISTINCT customer_id) AS 고객수
FROM customers
GROUP BY 국가명
ORDER BY 고객수 DESC
) AS a
) AS b;
1.3 구매이력이 없는 고객
1) LEFT JOIN 활용
SELECT c.customer_id
, c.company_name
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
2) EXCEPT (차집합) 활용
- 차집합으로 묶인 테이블의 컬럼 수가 동일해야 함
- 각 상호 비교되는 컬럼들의 데이터 형식이 호환되어야 함
(참고 포스팅 : https://rocabilly.tistory.com/50)
✔ 조인하지 않은 테이블끼리 except
SELECT customer_id
FROM customers c
EXCEPT
SELECT customer_id
FROM orders o;
✔ inner join한 테이블 except
→ company_name도 함께 출력하기 위해 inner join
(SELECT customer_id
, company_name
FROM customers c)
EXCEPT
(SELECT c.customer_id
, c.company_name
FROM customers c
INNER JOIN orders o
ON c.customer_id = o.customer_id);
1.4 기본 테이블 만들기
- 총 5개의 테이블 조인 : customers, orders, order_details, products, categories
✔ INNER JOIN
WITH cte_customers AS (
SELECT c.customer_id, c.company_name, c.contact_title, c.country, c.city
, o.order_id, o.order_date
, to_char(o.order_date, 'YYYY') AS YEAR
, to_char(o.order_date, 'MM') AS month
, to_char(o.order_date, 'DD') AS day
, to_char(o.order_date, 'Q') AS quarter
, od.product_id, od.unit_price, od.quantity, od.discount
, od.unit_price * od.quantity * (1-od.discount) AS sales
, p.product_name
, c2.category_id, c2.category_name
FROM customers c, orders o , order_details od , products p , categories c2
WHERE c.customer_id = o.customer_id
AND o.order_id = od.order_id
AND od.product_id = p.product_id
AND p.category_id = c2.category_id)
SELECT count(*) -- 2155
FROM cte_customers;
✔ LEFT JOIN
WITH cte_customers_left_join AS (
SELECT c.customer_id, c.company_name, c.contact_title, c.country, c.city
, o.order_id, o.order_date
, to_char(o.order_date, 'YYYY') AS YEAR
, to_char(o.order_date, 'MM') AS month
, to_char(o.order_date, 'DD') AS day
, to_char(o.order_date, 'Q') AS quarter
, od.product_id, od.unit_price, od.quantity, od.discount
, od.unit_price * od.quantity * (1-od.discount) AS sales
, p.product_name
, c2.category_id, c2.category_name
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
LEFT JOIN order_details od
ON o.order_id = od.order_id
LEFT JOIN products p
ON p.product_id = od.product_id
LEFT JOIN categories c2
ON c2.category_id = p.category_id)
SELECT count(*) -- 2157
FROM cte_customers_left_join;
1.5 국가별 고객 수, 매출액, 주문건수 상관계수
1) 국가별 고객수, 매출액, 주문건수
WITH cte_customers AS (
-- 생략 --)
, cte_country_custcnt_sales_ordercnt AS (
SELECT country AS 국가명
, count(DISTINCT customer_id) AS 고객수
, sum(sales) AS 매출액
, count(DISTINCT order_id) AS 주문건수
FROM cte_customers
GROUP BY country)
SELECT sum(고객수)
, sum(매출액)
, sum(주문건수)
FROM cte_country_custcnt_sales_ordercnt;
2) 상관계수 구하기
WITH cte_customers AS (
-- 생략 --)
, cte_country_custcnt_sales_ordercnt AS (
-- 생략 --)
, cte_corr AS (
SELECT corr(고객수, 매출액) AS "고객수_매출액"
, corr(매출액, 주문건수) AS "매출액_주문건수"
, corr(고객수, 주문건수) AS "고객수_주문건수"
FROM cte_country_custcnt_sales_ordercnt)
SELECT *
FROM cte_corr;
3) 국가별 매출액 지도 시각화 (Tableau 활용)
→ 북미, 남미, 유럽에 분포하는 것을 확인할 수 있음
→ 아래의 지도 그래프만 봐서 어떤 지역(북미, 남미, 유럽)이 매출이 가장 높은지 확인하기 어렵다.
1.6 지역 별 매출 비교
1) 지역 컬럼 추가 (북미, 남미, 유럽)
CASE WHEN 구문을 사용하여 지역 컬럼을 추가한다.
(국가명이 대소문자가 섞여 있기 때문에 소문자로 변환하여 CASE WHEN 구문으로 작성)
WITH cte_customers AS (
-- 생략 --)
, cte_country_custcnt_sales_ordercnt AS (
-- 생략 --)
, cte_country_group AS (
SELECT *
, CASE WHEN lower(국가명) IN ('usa', 'canada', 'mexico') THEN 'NorthAmerica'
WHEN lower(국가명) IN ('brazil', 'venezuela', 'argentina') THEN 'SouthAmerica'
ELSE 'Europe'
END AS 지역
FROM cte_country_custcnt_sales_ordercnt)
SELECT *
FROM cte_country_group;
2) 지역 별 매출 비교
WITH cte_customers AS (
-- 생략 --)
, cte_country_custcnt_sales_ordercnt AS (
-- 생략 --)
, cte_country_group AS (
SELECT *
, CASE WHEN lower(국가명) IN ('usa', 'canada', 'mexico') THEN 'NorthAmerica'
WHEN lower(국가명) IN ('brazil', 'venezuela', 'argentina') THEN 'SouthAmerica'
ELSE 'Europe'
END AS 지역
FROM cte_country_custcnt_sales_ordercnt)
SELECT 지역
, sum(고객수) AS 고객수
, sum(매출액) AS 매출액
, sum(주문건수) AS 주문건수
FROM cte_country_group
GROUP BY 지역
ORDER BY sum(매출액) DESC;
3) 지역별 고객수, 매출액, 주문건수 구성비
WITH cte_customers AS (
-- 생략 --)
, cte_country_custcnt_sales_ordercnt AS (
-- 생략 --)
, cte_country_group AS (
-- 생략 --)
, cte_country_group_cnt AS (
SELECT 지역
, sum(고객수) AS 고객수
, sum(매출액) AS 매출액
, sum(주문건수) AS 주문건수
FROM cte_country_group
GROUP BY 지역
ORDER BY sum(매출액) DESC)
SELECT 지역
, 고객수
, 고객수 / sum(고객수) over() AS 고객수구성비
, 매출액
, 매출액 / sum(매출액) over() AS 매출액구성비
, 주문건수
, 주문건수 / sum(주문건수) over() AS 주문건수구성비
FROM cte_country_group_cnt;
→ 유럽이 고객수, 매출액, 주문건수가 가장 많은 것을 확인할 수 있다.
4) 지역별 판매된 제품 순위 (판매 수량 기준)
WITH cte_customers AS (
-- 생략 --)
, cte_country_category_sales AS (
SELECT country AS 국가명
, category_name AS 카테고리명
, product_id AS 제품ID
, product_name AS 제품명
, CASE WHEN lower(country) IN ('usa', 'canada', 'mexico') THEN 'NorthAmerica'
WHEN lower(country) IN ('brazil', 'venezuela', 'argentina') THEN 'SouthAmerica'
ELSE 'Europe'
END AS 지역
, sum(sales) AS 매출액
, sum(quantity) AS 판매수량
FROM cte_customers
GROUP BY 1, 2, 3, 4)
, cte_concat AS (
SELECT 지역
, '['||카테고리명||'] '||제품명||' ('||제품ID||')' AS 제품정보
, sum(매출액) AS 매출액
, sum(판매수량) AS 판매수량
FROM cte_country_category_sales
GROUP BY 1, 2)
, cte_rank AS (
SELECT ROW_NUMBER() over(PARTITION BY 지역 ORDER BY 판매수량 DESC) AS RANK
, 지역
, 제품정보
FROM cte_concat)
, cte_pivot AS (
SELECT rank
, max(CASE WHEN 지역 = 'NorthAmerica' THEN 제품정보 END) AS NorthAmerica
, max(CASE WHEN 지역 = 'SouthAmerica' THEN 제품정보 END) AS SouthAmerica
, max(CASE WHEN 지역 = 'Europe' THEN 제품정보 END) AS Europe
FROM cte_rank
GROUP BY 1
ORDER BY 1)
SELECT *
FROM cte_pivot;
→ 지역 별로 많이 판매된 제품이 다른 것을 확인할 수 있다.
→ 특히 유럽은 Daily Products 카테고리의 제품이 많이 판매되는 것을 확인할 수 있다.
5) 고객 별 매출액, 주문건수, 건당평균주문액
WITH cte_customers AS (
-- 생략 --)
, cte_sales AS (
SELECT customer_id
, count(DISTINCT order_id) AS 주문건수
, sum(sales) AS 매출액
, sum(sales) / count(DISTINCT order_id) AS 건당평균주문액
FROM cte_customers
GROUP BY 1
ORDER BY 3 desc)
SELECT customer_id
, 주문건수
, rank() over(ORDER BY 주문건수 desc) AS 주문건수순위
, 매출액
, rank() over(ORDER BY 매출액 desc) AS 매출액순위
, 건당평균주문액
, rank() over(ORDER BY 건당평균주문액 desc) AS 건당평균주문액순위
FROM cte_sales
ORDER BY 매출액순위;
→ 매출액이 높은 고객은 건당평균주문액도 높은 것을 확인할 수 있다.
6) 시각화
✔ 매출액-건당평균주문액
✔ 매출액-주문건수
2. Decil 분석
① 고객의 총 매출액 기준으로 정렬
② 상위부터 10%씩 나누어 10개의 그룹 할당
③ decil 별 매출합계
④ decil 별 구성비
⑤ decil 별 구성비 누계
1) 쿼리 작성 및 데이터 추출
WITH cte_customers AS (
-- 생략 --)
-- ① 고객의 총 매출액 기준으로 정렬
, cte_sales AS (
SELECT customer_id
, sum(sales) AS 매출액
FROM cte_customers
GROUP BY customer_id
ORDER BY 2 desc)
-- ② 상위부터 10%씩 나누어 10개의 그룹 할당 (ntile 함수 사용)
, cte_ntile AS (
SELECT *
, NTILE(10) over(ORDER BY 매출액 desc) AS decil
FROM cte_sales)
-- ③ decil 별 매출합계
, cte_decil_sales AS (
SELECT decil
, sum(매출액) AS 매출액
FROM cte_ntile
GROUP BY 1
ORDER BY 1)
-- ④ decil 별 구성비
, cte_decil_sales_per AS (
SELECT *
, sum(매출액) OVER() AS 전체매출액
, 매출액 / sum(매출액) OVER() * 100 AS 전체매출액구성비
FROM cte_decil_sales)
-- ⑤ decil 별 구성비 누계
, cte_decil_sales_per_cumsum AS (
SELECT *
, sum(전체매출액구성비) OVER(ORDER BY 전체매출액구성비 desc) AS 전체매출액구성비누계
FROM cte_decil_sales_per)
SELECT *
FROM cte_decil_sales_per_cumsum;
2) decil의 문제점
- 한 번의 구매로 비싼 물건을 구매한 사용자와 정기적으로 저렴한 물건을 여러 번 구매한 사용자가 같은 그룹으로 판정되는 문제
- 검색기간이 너무 장기간이면 과거에는 우수고객이었어도 현재는 다른 서비스를 사용하는 휴면고객이 포함될 수 있음
- 검색기간이 너무 단기간이면 정기적으로 구매하는 안정고객보다 해당 기간동안 일시적으로 많이 구매한 사용자가 우수고객으로 포함될 수 있음
3. RFM 분석
- 구매 가능성이 높은 고객을 식별하기 위한 데이터 분석 방법
- 마케팅에서 사용자 타겟팅을 위한 방법
- decil의 문제점을 보완하기 위해 활용하는 방법
- Recency : 얼마나 최근에 구매했는가?
- Frequency : 얼마나 빈번하게 구매했는가?
- Monetary : 얼마나 많은 금액을 지불했는가?
✔ 쿼리 작성
- 기준일 : order_date 최대값
- 집계기간 : 전체기간
- Recency : 기준일 대비 며칠이 기났는가
- Frequency : 총구매횟수
- Monetary : 총구매액
WITH cte_customers AS (
SELECT c.customer_id, c.company_name
, o.order_id, o.order_date
, od.product_id, od.unit_price, od.quantity, od.discount
, od.unit_price * od.quantity * (1-od.discount) AS sales
FROM customers c, orders o , order_details od
WHERE c.customer_id = o.customer_id
AND o.order_id = od.order_id)
-- 1) 기준일 설정
, cte_max_date AS (
SELECT *
, max(order_date) over() AS 기준일
, max(order_date) over() - order_date AS diff
FROM cte_customers)
-- 2) RFM 계산
SELECT customer_id
, min(diff) AS R
, count(DISTINCT order_id) AS F
, sum(sales) AS M
FROM cte_max_date
GROUP BY 1
ORDER BY 4 desc;
4. 과제
☑ left join 한 기본 테이블을 사용하여 분석
☑ 고객별 ABC분석
4.1 left join 한 기본 테이블을 사용하여 분석
1) 직책 별 고객 수, 매출액, 주문건수
-- 1) 기본 테이블 만들기 (left join)
WITH cte_customers_left_join AS (
SELECT c.customer_id, c.company_name, c.contact_title, c.country, c.city
, o.order_id, o.order_date
, to_char(o.order_date, 'YYYY') AS YEAR
, to_char(o.order_date, 'MM') AS month
, to_char(o.order_date, 'DD') AS day
, to_char(o.order_date, 'Q') AS quarter
, od.product_id, od.unit_price, od.quantity, od.discount
, od.unit_price * od.quantity * (1-od.discount) AS sales
, p.product_name
, c2.category_id, c2.category_name
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
LEFT JOIN order_details od
ON o.order_id = od.order_id
LEFT JOIN products p
ON p.product_id = od.product_id
LEFT JOIN categories c2
ON c2.category_id = p.category_id)
-- 2) contact_title 별 고객수, 고객당매출액, 주문건수, 주문건당매출액, 매출액
, cte_title_sales AS (
SELECT contact_title
, count(DISTINCT customer_id) AS 고객수
, sum(sales) / count(DISTINCT customer_id) AS 고객당매출액
, count(DISTINCT order_id) AS 주문건수
, sum(sales) / count(DISTINCT order_id) AS 주문건당매출액
, sum(sales) AS 매출액
FROM cte_customers_left_join
GROUP BY 1
ORDER BY 매출액 DESC)
-- 3) 매출액, 고객수, 주문건수 상관관계
SELECT corr(매출액, 고객수) AS "매출액-고객수"
, corr(매출액, 주문건수) AS "매출액-주문건수"
, corr(고객수, 주문건수) AS "고객수-주문건수"
FROM cte_title_sales;
→ 매출액 - 주문건수 & 고객수 - 주문건수의 상관관계가 매우 높은 것을 확인할 수 있다.
2) 카테고리 별 고객수, 매출액, 주문건수
-- 1) 기본 테이블 만들기 (left join)
WITH cte_customers_left_join AS (
-- 생략 --)
-- 2) 카테고리 별 고객수, 매출액, 주문수 계산
, cte_category_sales AS (
SELECT category_name
, count(DISTINCT customer_id) AS 고객수
, sum(sales) AS 매출액
, count(DISTINCT order_id) AS 주문수
FROM cte_customers_left_join
GROUP BY 1
ORDER BY 매출액 desc)
SELECT *
FROM cte_category_sales
WHERE category_name IS NOT NULL;
→ Beverages, Daily Products의 카테고리 상품을 구매한 고객 수, 매출액, 주문수가 가장 많은 것을 확인할 수 있다.
3) 고객 별 매출액, 주문건수, 건당평균주문액
-- 1) 기본 테이블 만들기 (left join)
WITH cte_customers_left_join AS (
-- 생략 --)
-- 2) 고객 별 주문수, 매출액, 건당평균주문액 계산 (주문하지 않은 고객은 출력x)
, cte_sales AS (
SELECT customer_id
, count(DISTINCT order_id) AS 주문수
, sum(sales) AS 매출액
, sum(sales) / count(DISTINCT order_id) AS 건당평균주문액
FROM cte_customers_left_join
GROUP BY 1
HAVING sum(sales) IS NOT NULL
ORDER BY 3 DESC)
-- 3) 고객 별 주문수, 매출액, 건당평균주문액, 순위
SELECT customer_id
, 주문수
, rank() over(ORDER BY 주문수 desc) AS 주문수순위
, 매출액
, rank() over(ORDER BY 매출액 desc) AS 매출액순위
, 건당평균주문액
, rank() over(ORDER BY 건당평균주문액 desc) AS 건당평균주문액순위
FROM cte_sales
ORDER BY 매출액순위;
→ QUICK 고객이 매출액과 건당 평균주문액 순위가 가장 많은 것을 확인할 수 있다.
주문 수는 3위이지만 평균주문액 순위가 1위인것으로 보아 한 주문 당 주문액이 다른 고객에 비해 많은 것을 알 수 있다.
4.2 고객 별 ABC 분석
1) Decil 분석
WITH customers_sales AS (
SELECT c.customer_id
, od.unit_price * od.quantity * (1-od.discount) AS sales
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id
LEFT JOIN order_details od
ON o.order_id = od.order_id)
-- ① 고객의 총 매출액 기준으로 정렬
, cte_sales AS (
SELECT customer_id
, sum(sales) AS total_sales
FROM customers_sales
GROUP BY 1
HAVING sum(sales) IS NOT NULL
ORDER BY 2 DESC)
-- ② 상위부터 10%씩 나누어 10개의 그룹 할당 (ntile 함수 사용)
, cte_decil AS (
SELECT *
, ntile(10) over(ORDER BY total_sales desc) AS decil
FROM cte_sales)
-- ③ decil 별 매출합계
, cte_decil_sales AS (
SELECT decil
, sum(total_sales) AS 매출합계
FROM cte_decil
GROUP BY 1
ORDER BY 1)
-- ④ decil 별 구성비
, cte_decil_sales_per AS (
SELECT *
, sum(매출합계) over() AS 매출총합
, 매출합계 / sum(매출합계) over()* 100 AS 구성비
FROM cte_decil_sales)
-- ⑤ decil 별 구성비 누계
SELECT decil
, 매출합계
, 구성비
, sum(구성비) over(ORDER BY 구성비 desc) AS 구성비누계
FROM cte_decil_sales_per;
2) decil을 활용한 ABC 분석
→ decil이 7까지는 등급 'A', 9까지는 등급 'B', 나머지는 등급 'C'로 진행
WITH customers_sales AS (
-- 생략 --)
-- ① 고객의 총 매출액 기준으로 정렬
, cte_sales AS (
SELECT customer_id
, sum(sales) AS total_sales
FROM customers_sales
GROUP BY 1
HAVING sum(sales) IS NOT NULL
ORDER BY 2 DESC)
-- ② 상위부터 10%씩 나누어 10개의 그룹 할당 (ntile 함수 사용)
, cte_decil AS (
SELECT *
, ntile(10) over(ORDER BY total_sales desc) AS decil
FROM cte_sales)
-- ③ decil 별 매출합계
, cte_decil_sales AS (
SELECT decil
, customer_id
, sum(total_sales) AS 매출합계
FROM cte_decil
GROUP BY 1, 2
ORDER BY 3 desc)
SELECT *
, CASE WHEN decil <= 7 THEN 'A'
WHEN decil <= 9 THEN 'B'
ELSE 'C' END AS 등급
FROM cte_decil_sales;
3) ABC 분석
WITH customers_sales AS (
SELECT c.customer_id
, od.unit_price * od.quantity * (1-od.discount) AS sales
FROM customers c
LEFT JOIN orders o
ON c.customer_id = o.customer_id
LEFT JOIN order_details od
ON o.order_id = od.order_id)
-- 1) 고객 별 매출액 계산
, cte_sales AS (
SELECT customer_id
, sum(sales) AS 매출액
FROM customers_sales
GROUP BY 1
HAVING sum(sales) IS NOT NULL
ORDER BY 2 desc)
-- 2) 고객 별 매출 비율 계산
, cte_sales_percent AS (
SELECT *
, sum(매출액) over() AS 전체매출액
, 매출액 / sum(매출액) over() * 100 AS 매출비율
FROM cte_sales)
-- 3) 고객 별 매출 비율 누계 계산
, cte_sales_percent_cumnum AS (
SELECT customer_id
, 매출액
, 매출비율
, sum(매출비율) over(ORDER BY 매출비율 desc) AS 매출비율누계
FROM cte_sales_percent)
SELECT *
, CASE WHEN 매출비율누계 <= 70 THEN 'A'
WHEN 매출비율누계 <= 90 THEN 'B'
ELSE 'C'
END AS 등급
FROM cte_sales_percent_cumnum;
[+] 시각화
💡 회고
새로운 이론과 더불어 지난 주까지 배웠던 것들을 활용하며 쿼리를 짰다.
점점 쿼리 짜는 것에 익숙해지기 시작했다. 하지만 아직도 어려운 부분은 집계함수 사용, group by 사용 등이다.
이와 관련된 에러 메시지가 자주 떠서 헷갈리는데, 이런 부분들을 조금씩 보완하면 괜찮지 않을까 싶다.
그리고 사람들이랑 같이 SQL 쿼리 테스트 문제를 풀고 있다.
지금 배우고 있는 SQL 쿼리도 반복하면 좋지만 다양한 쿼리를 짜면서 SQL 쿼리 짜는 것에 다방면으로 익숙해지기 위함이다.
일주일정도 하다보면 좀 더 익숙해지지 않을까 싶다 :)
'웅진X유데미 STARTERS > TIL (Today I Learned)' 카테고리의 다른 글
[스타터스 TIL] 52일차.SQL 실전 트레이닝 (8) - 미니프로젝트 1 (0) | 2023.04.22 |
---|---|
[스타터스 TIL] 51일차.SQL 실전 트레이닝 (7) - RFM 분석, 재구매율, 이탈고객 분석, 백분위수, 최빈값 (0) | 2023.04.18 |
[스타터스 TIL] 49일차.SQL 실전 트레이닝 (5) - Z차트, 그룹함수 (GROUPING SETS, ROLLUP, CUBE) (0) | 2023.04.14 |
[스타터스 TIL] 48일차.SQL 실전 트레이닝 (4) - 제품/카테고리 매출 지표 분석, ABC 분석 (0) | 2023.04.13 |
[스타터스 TIL] 47일차.SQL 실전 트레이닝 (3) - CTE, 윈도우 함수 (0) | 2023.04.13 |