Google Big Query
- 구글에서 제공하는 클라우드 데이터 웨어하우스
- Data Warehouse : 분석용 중앙 데이터 창고의 개념
- 서비스와 직접적으로 연결되어 있는 DB가 아닌 분석용 DB를 사용
- SQL 활용
- 빅쿼리 접속 URL : https://console.cloud.google.com/bigquery
- 프로젝트 제작
- 프로젝트 만들기 - 이름 설정 - 만들기
- 프로젝트 구조
- 프로젝트 - 데이터세트 - 테이블
- 쿼리
- SQL 쿼리 만들기 - 쿼리 입력창 / 쿼리 결과창
- 쿼리 실행
- ; 사용해 여러 쿼리 실행 가능
- 탭을 추가해서 여러 쿼리 작성 가능
- 원하는 쿼리만 드래그해서 실행하는 것도 가능
- 테이블 내에서 쿼리 버튼을 통해 바로 테이블이 FROM절에 세팅되도록 하는 것도 가능
- 데이터셋 업로드
- 프로젝트 ID - 점 세개 - 데이터 세트 만들기
- ID 설정 - 리전(서울) - 데이터 세트 만들기
- 데이터 세트 이동 - 테이블 만들기
- 테이블 업로드
- 소스 - 업로드 (customers, items, orders, products, category_name)
- 테이블 이름 설정
- 스키마 - 자동 감지 (수동 설정도 가능) -> 테이블 만들기
- ** 특수 테이블
- 번역 파일 (category_name)
- 스키마 직접 작성
- 지표 쪼개기
- 큰 지표 -> 작은 지표 (디테일)
- 큰 지표를 설정하고 이 큰 지표를 추가로 확인할 수 있는 작은 지표로 쪼개서 분석
- 매출 = 주문건수 건당 평균 주문 금액
- 주문건수 = 주문고객수 주문빈도
- 건당 평균 주문 금액 = 판매품목수 * 물품평균가격
- 쿼리 분석
- 매출 = 판매 금액 총합
- olist_order_items -> price
- 주문 수 = 주문 건수 세기
- olist_order_items -> order_id
- 건당 주문 금액 = 판매 금액 총합 / 주문 건수 세기
- 쿼리 ```sql
SELECT
SUM(price) AS ord_amt, -- 주문 금액 총합 (매출)
COUNT(DISTINCT order_id) AS unique_orders, -- 전체 주문 건수
SUM(price) / COUNT(DISTINCT order_id) AS amount_per_order
-- 주문 금액 총합 / 주문 건수 (주문 1건당 평균 가격)
FROM `olist.olist_order_items` ;
- 주문 수 = 고객 수 * 주문빈도
- 고객 수
- olist_customers -> customer_unique_id
- olist_orders -> customer_id
- 2개 테이블 조인
- 쿼리
```sql
/* 주문 고객 unique 세기 */
SELECT
COUNT(DISTINCT ord.order_id) AS ord_cnt, -- 주문 건수
COUNT(DISTINCT ord.customer_id) AS cust_cnt,
-- 참고. 주문건의 고객 id 세기 (주문건수와 동일)
COUNT(DISTINCT cust.customer_unique_id) AS unique_cust_cnt
-- 고객 고유 id 세기
FROM `olist.olist_orders` AS ord -- 주문정보 테이블
LEFT JOIN `olist.olist_customers` AS cust -- 고객정보 테이블
ON ord.customer_id = cust.customer_id ; -- join 조건 : 주문 건의 고객 id
```sql
/* 주문 빈도까지 조회 */
SELECT
COUNT(DISTINCT ord.order_id) AS ord_cnt, -- 주문 건수
COUNT(DISTINCT ord.customer_id) AS cust_cnt,
-- 참고. 주문건의 고객 id 세기 (주문건수와 동일)
COUNT(DISTINCT cust.customer_unique_id) AS unique_cust_cnt,
-- 고객 고유 id 세기
COUNT(DISTINCT order_id) / COUNT(DISTINCT cust.customer_unique_id) AS frequency -- 주문빈도
FROM `olist.olist_orders` AS ord -- 주문정보 테이블
LEFT JOIN `olist.olist_customers` AS cust -- 고객정보 테이블
ON ord.customer_id = cust.customer_id ; -- join 조건 : 주문 건의 고객 id
- ++ 한글로 Alias 설정하고 싶을 때는 AS `한글`의 형태로 (` 백틱) 입력
- 건당 주문 금액 = 주문 건당 판매 품목 수 * 물품 평균 가격
- 주문 건당 판매 품목 수 = 총 판매 상품 수 / 주문 건수
- 물품 평균 가격 = 매출 / 총 판매 상품 수
- 쿼리
- 주문건수 : olist_order_items의 고유 order_id 수 세기
- 총 판매 상품 수 : 판매 품목 수 세기 order_item_id
- 매출 : 판매가격 전부 더하기 price
```sql
/* 총 매출, 주문건수 집계 */
SELECT
SUM(price) AS ord_amt, -- 주문 금액 총합 (매출)
COUNT(DISTINCT order_id) AS unique_orders, -- 전체 주문 건수
SUM(price) / COUNT(DISTINCT order_id) AS amount_per_order
-- 주문 금액 총합 / 주문 건수 (주문 1건당 평균 가격)
FROM `olist.olist_order_items` ;
```sql
/* 총 판매 상품 수, 주문건수 집계 */
SELECT
COUNT(order_item_id) AS prd_cnt, -- 총 판매 상품 수
COUNT(DISTINCT order_id) AS ord_cnt, -- 주문 건수
COUNT(order_item_id) / COUNT(DISTINCT order_id) AS unit_per_order
-- 주문 당 평균 판매 상품 수
FROM `olist.olist_order_items` AS ord ; -- 주문상품정보 테이블
```sql
/* 물품 평균 가격 집계 */
SELECT
SUM(price) AS ord_amt, -- 주문 금액 총합 (매출)
COUNT(order_item_id) AS prd_cnt, -- 총 판매 상품 수
SUM(price) / COUNT(order_item_id) AS average_selling_price
-- 판매 제품 당 평균 가격
FROM `olist.olist_order_items` AS ord ; -- 주문상품정보 테이블
```sql
/* 합치기 */
SELECT
SUM(price) AS ord_amt, -- 주문 금액 총합 (매출)
COUNT(DISTINCT order_id) AS unique_orders, -- 전체 주문 건수
COUNT(order_item_id) AS prd_cnt, -- 총 판매 상품 수
SUM(price) / COUNT(DISTINCT order_id) AS amount_per_order
-- 주문 금액 총합 / 주문 건수 (주문 1건당 평균 가격)
COUNT(order_item_id) / COUNT(DISTINCT order_id) AS unit_per_order
-- 주문 당 평균 판매 상품 수
SUM(price) / COUNT(order_item_id) AS average_selling_price
-- 판매 제품 당 평균 가격
FROM `olist.olist_order_items` AS ord ; -- 주문상품정보 테이블
- 종합 쿼리
- 원하는 지표를 여러 테이블을 조인해 하나의 테이블로 표시
1. 주문 1건 당 주문금액, 판매상품의 수 구하기
2. 주문 정보 테이블에 1번 결과와 고객 unique ID를 붙이기
3. 2번 결과 집계
1. 주문 1건 당 주문금액, 판매상품의 수 구하기
- 쿼리
```sql
SELECT
item.order_id,
SUM(item.price) as ord_amt,
COUNT(item.order_item_id) as prd_cnt
FROM `olist.olist_order_items` AS item -- 주문상품정보 테이블
GROUP BY item.order_id -- 집계 기준 : 주문 번호
2. 주문 정보 테이블에 1번 결과와 고객 unique ID를 붙이기
- 쿼리
```sql
WITH tb AS(
SELECT
item.order_id,
SUM(item.price) as ord_amt,
COUNT(item.order_item_id) as prd_cnt
FROM `olist.olist_order_items` AS item -- 주문상품정보 테이블
GROUP BY item.order_id -- 집계 기준 : 주문 번호
),
SELECT
ord.order_id,
ord.order_status,
cust.customer_unique_id,
tb.ord_amt,
tb.prd_cnt
FROM `olist.olist_orders` AS ord -- 주문정보 테이블
LEFT JOIN `olist.olist_customers` AS cust -- 고객정보 테이블
ON ord.customer_id = cust.customer_id -- join 조건 : 주문 건의 고객 id
INNER JOIN tb
ON ord.order_id = tb.order_id ;
3. 2번 결과 집계
- 쿼리
```sql
WITH tb AS(
SELECT
item.order_id,
SUM(item.price) as ord_amt,
COUNT(item.order_item_id) as prd_cnt
FROM `olist.olist_order_items` AS item -- 주문상품정보 테이블
GROUP BY item.order_id -- 집계 기준 : 주문 번호
)
, base AS(
SELECT
ord.order_id,
ord.order_status,
cust.customer_unique_id,
tb.ord_amt,
tb.prd_cnt
FROM `olist.olist_orders` AS ord -- 주문정보 테이블
LEFT JOIN `olist.olist_customers` AS cust -- 고객정보 테이블
ON ord.customer_id = cust.customer_id -- join 조건 : 주문 건의 고객 id
INNER JOIN tb
ON ord.order_id = tb.order_id
)
SELECT
SUM(ord_amt) AS `총 매출`,
COUNT(DISTINCT order_id) AS `총 주문수`,
SUM(prd_cnt) AS `총 판매상품수`,
SUM(ord_amt) / COUNT(DISTINCT order_id) AS `주문 당 평균가격`,
SUM(prd_cnt) / COUNT(DISTINCT order_id) AS `평균 판매상품수`,
SUM(ord_amt) / SUM(prd_cnt) AS `제품 개당 평균가격`,
COUNT(DISTINCT customer_unique_id) AS `주문 고객수`,
COUNT(DISTINCT order_id) / COUNT(DISTINCT customer_unique_id) AS `주문빈도`
FROM base ;
- 날짜
- DATE : 2017-11-25
- 쿼리 ```sql
SELECT
DATE(2016, 12, 25) AS date_ymd,
DATE(DATETIME '2016-12-25 23:59:59') AS date_dt,
DATE(TIMESTAMP '2016-12-25 05:30:00+09', 'Asia/Seoul') AS date_tstz ;
- DATETIME : 2017-11-25T11:10:33
- 쿼리
```sql
SELECT
DATETIME(2016, 12, 25, 05, 30, 00) AS datetime_ymdhms,
DATETIME(TIMESTAMP '2016-12-25 05:30:00+09', 'Asia/Seoul') AS datetime_tstz ;
- TIMESTAMP : 2017-11-25 11:10:33 UTC
- 타임존 포함 (한국 : UTC + 9)
- TIME : 11:10:33
- 빅쿼리 내 실제 쿼리
```sql
SELECT
order_purchase_timestamp,
DATE(order_purchase_timestamp) AS ord_date,
DATETIME(order_purchase_timestamp) AS ord_date,
TIMESTAMP(order_purchase_timestamp) AS ord_ts,
TIMESTAMP(DATE(order_purchase_timestamp)) AS ord_date_ts,
TIME(order_purchase_timestamp) AS ord_time
FROM `olist.olist_orders` ;
- 날짜 일부분 추출
- EXTRACT('추출할 부분' FROM '날짜 컬럼') -> 실행 시 숫자로 변경
- 추출할 부분
- YEAR : 연도
- MONTH : 월
- DAY : 일
- DAYOFWEEK : 요일 (0:일요일 ~ 6:토요일)
- WEEK : 주(기본 일요일 시작) - 0주부터 시작
- WEEK(MONDAY) : 주(월요일 시작) - 0주부터 시작
- QUARTER : 분기
- HOUR : 시
- MINUTE : 분
- SECOND : 초
```sql
SELECT
order_purchase_timestamp,
EXTRACT(YEAR FROM order_purchase_timestamp)
FROM `olist.olist_orders` ;
```
- 날짜에서 원하는 정보까지만 남길 때
- DATE_TRUNC('날짜', '남기는 부분')
- DATETIME_TRUNC('날짜', '남기는 부분')
- TIMESTAMP_TRUNC('날짜', '남기는 부분')
- 남기는 부분
- YEAR : 해당 연도의 첫날
- MONTH : 해당 월의 첫날
- DAY : 해당 날짜의 0시 0분
- WEEK : 해당 주차의 첫 날 (일요일)
- WEEK(MONDAY) : 해당 주차의 첫 날 (월요일)
- QUARTER : 분기
- HOUR : 시
- MINUTE : 분
- SECOND : 초
```sql
SELECT
order_purchase_timestamp,
TIMESTAMP_TRUNC(order_purchase_timestamp, YEAR)
FROM `olist.olist_orders` ;
```
- 종합 쿼리에 날짜 값 활용
```sql
WITH tb AS(
SELECT
item.order_id,
SUM(item.price) as ord_amt,
COUNT(item.order_item_id) as prd_cnt
FROM `olist.olist_order_items` AS item -- 주문상품정보 테이블
GROUP BY item.order_id -- 집계 기준 : 주문 번호
)
, base AS(
SELECT
ord.order_approved_at,
DATE(ord.order_approved_at) AS ord_date,
DATETIME(ord.order_approved_at) AS ord_dt,
TIMESTAMP(ord.order_approved_at) AS ord_ts,
EXTRACT(YEAR FROM ord.order_approved_at) AS ord_year,
EXTRACT(MONTH FROM ord.order_approved_at) AS ord_month,
TIMESTAMP_TRUNC(ord.order_approved_at, YEAR) AS ord_year_t,
TIMESTAMP_TRUNC(ord.order_approved_at, MONTH) AS ord_month_t,
ord.order_id,
ord.order_status,
cust.customer_unique_id,
tb.ord_amt,
tb.prd_cnt
FROM `olist.olist_orders` AS ord -- 주문정보 테이블
LEFT JOIN `olist.olist_customers` AS cust -- 고객정보 테이블
ON ord.customer_id = cust.customer_id -- join 조건 : 주문 건의 고객 id
INNER JOIN tb
ON ord.order_id = tb.order_id
)
SELECT
ord_year,
SUM(ord_amt) AS `총 매출`,
COUNT(DISTINCT order_id) AS `총 주문수`,
SUM(prd_cnt) AS `총 판매상품수`,
SUM(ord_amt) / COUNT(DISTINCT order_id) AS `주문 당 평균가격`,
SUM(prd_cnt) / COUNT(DISTINCT order_id) AS `평균 판매상품수`,
SUM(ord_amt) / SUM(prd_cnt) AS `제품 개당 평균가격`,
COUNT(DISTINCT customer_unique_id) AS `주문 고객수`,
COUNT(DISTINCT order_id) / COUNT(DISTINCT customer_unique_id) AS `주문빈도`
FROM base
GROUP BY ord_year
ORDER BY ord_year ;
- 유용한 함수
- 원하는 컬럼만 불러오기
- EXCEPT : 필요 없는 컬럼은 빼고 불러옴
- SELECT EXCEPT('제외할 컬럼')
- REPLACE : 기존 컬럼을 대체해서 표시됨
- SELECT REPLACE('대체할 값' AS '컬럼명')
- EX) SELECT REPLACE(price 10000 AS price)
- 데이터 타입 변경
- CAST('값' AS '데이터타입')
- EX)
- CAST('123' AS INT64)
- CAST('abc' AS INT64) -> error
- SAFE_CAST('값' AS '데이터타입')
- EX)
- SAFE_CAST('123' AS INT64)
- SAFE_CAST('abc' AS INT64) -> null
- ++ SAFE 활용 -> error가 아닌 Null 반환
- SAFE_ADD(X, Y) = X + Y
- SAFE_SUBTRACT(X, Y) = X - Y
- SAFE_MULTIPLY(X, Y) = X * Y
- SAFE_DIVIDE(X, Y) = X / Y -> 0으로 나누는 경우에 활용
- IFNULL과 조합하면 null 값을 다른 값으로 변경하는 것도 가능
- IFNULL('값', '변경 값')
- 자주 쓰이는 IN / NOT IN
- IN
- WHERE '컬럼' IN (A, B, C)
- NOT IN
- WHER '컬럼' NOT IN (A, B, C)
- 쿼리 ```sql
WITH tb AS(
SELECT
item.order_id,
SUM(item.price) as ord_amt,
COUNT(item.order_item_id) as prd_cnt
FROM `olist.olist_order_items` AS item -- 주문상품정보 테이블
GROUP BY item.order_id -- 집계 기준 : 주문 번호
)
, base AS(
SELECT
DATE(ord.order_approved_at) AS ord_date,
ord.order_id,
ord.order_status,
cust.customer_unique_id,
tb.ord_amt,
tb.prd_cnt
FROM `olist.olist_orders` AS ord -- 주문정보 테이블
LEFT JOIN `olist.olist_customers` AS cust -- 고객정보 테이블
ON ord.customer_id = cust.customer_id -- join 조건 : 주문 건의 고객 id
INNER JOIN tb
ON ord.order_id = tb.order_id
WHERE 1 = 1
AND order_status IN ('delivered', 'shipped')
AND order_approved_at IS NOT NULL
)
SELECT
ord_date,
ROUND(SUM(ord_amt), 2) AS ord_amt, --총 매출
COUNT(DISTINCT order_id) AS ord_cnt, --총 주문수
SUM(prd_cnt) AS prd_cnt, --총 판매상품수
ROUND(IFNULL(SAFE_DIVIDE(SUM(ord_amt), COUNT(DISTINCT order_id)), 0), 2) AS avg_ord_amt, --주문 당 평균가격
ROUND(IFNULL(SAFE_DIVIDE(SUM(prd_cnt), COUNT(DISTINCT order_id)), 0), 2) AS avg_prd_cnt, --평균 판매상품수
ROUND(IFNULL(SAFE_DIVIDE(SUM(ord_amt) , SUM(prd_cnt)), 0), 2) AS avg_price, --제품 개당 평균가격
COUNT(DISTINCT customer_unique_id) AS cust_cnt, --주문 고객수
ROUND(IFNULL(SAFE_DIVIDE(COUNT(DISTINCT order_id), COUNT(DISTINCT customer_unique_id)), 0), 2) AS cust_freq --주문빈도
FROM base
GROUP BY ord_date
ORDER BY ord_date ;
- AD HOC QUERY
- 요청사항 1
- 주(state)별 침투율 확인 및 침투율이 낮은 지역을 중심으로 침투율 개선 예정
- 주별 주문수와 고객수 추출 필요
```sql
/* 주(state)별 주문수, 고객수를 추출합니다. */
-- 고객 정보 기준
-- 주문 상태는 필터링하지 않음
-- olist_orders, olist_customers 사용
SELECT
customer_state,
COUNT(DISTINCT ord.order_id) AS ord_cnt,
COUNT(DISTINCT cust.customer_unique_id) AS cust_cnt
FROM `olist.olist_orders` AS ord
LEFT JOIN `olist.olist_customers` AS cust
ON ord.customer_id = cust.customer_id
GROUP BY customer_state
ORDER BY cust_cnt DESC ;
```
- 요청사항 2
- 고객수 Top 2인 상파울루(SP), 리우데자네이루(RJ) 주만 필터링 (서브쿼리를 위한 join 연습)
```sql
/* 요청사항 2. 고객수 Top2 SP, RJ만 주문 추출 */
-- 인라인 서브 쿼리 사용
-- olist_orders, olist_customers 사용
SELECT
ord.order_id,
cust.customer_state
FROM `olist.olist_orders` AS ord
LEFT JOIN (SELECT customer_id,
customer_unique_id,
customer_state
FROM `olist.olist_customers`
WHERE customer_state IN ('SP', 'RJ')) AS cust
ON ord.customer_id = cust.customer_id
WHERE cust.customer_state IS NOT NULL ;
-- WHERE 절은 olist_orders 테이블 기준 LEFT JOIN 이기 떄문에 olist_custmers 쪽 NULL 값 제외
```
- 요청사항 3
- 현재 2018년 가정
- 2017년 도시(city)별 주문수와 고객수, 매출 추출
- 고객 수 기준 순위 표시
- 전체 순위, 주(state) 내 순위 표시
- 배송완료(delivered)된 건만 집계
- 순위 쿼리
- ROW_NUMBER() : 중복 없는 순위 (고유)
- Ex) ROW_NUMBER() OVER (ORDER BY 컬럼 DESC)
- RANK() : 동점일 때 중복 순위 (공동 등수)
- 공동 순위자가 있다면 그 수 만큼 건너뛰고 다음 등수
- DENSE_RANK() : 동점일 때 중복 순위 (공동 등수)
- 공동 순위자가 있어도 연속으로 등수 설정
- 사용법
- OVER (ORDER BY [순위 조건])
- 순위 조건 : 컬럼 & 오름차순, 내림차순 (ASC, DESC)
- OVER (PARTITION BY [구분 기준] ORDER BY [순위 조건])
- 구분 기준 : 구분이 필요한 컬럼
```sql
/* 요청사항 3. 2017년 도시(city)별 주문수, 고객수, 매출 */
-- 현재 2018년 가정
-- 고객 수 기준 내림차순 순위
-- 순위는 전체 순위, 주(state) 기준 순위 모두 제공
-- 배송완료 (order_status = 'delivered') 건만 집계
WITH tb AS(
SELECT
item.order_id,
SUM(item.price) as ord_amt
FROM `olist.olist_order_items` AS item -- 주문상품정보 테이블
GROUP BY item.order_id -- 집계 기준 : 주문 번호
)
, base AS(
SELECT
cust.customer_state,
cust.customer_city,
COUNT(DISTINCT ord.order_id) AS ord_cnt,
COUNT(DISTINCT cust.customer_unique_id) AS cust_cnt,
SUM(tb.ord_amt) AS ord_amt
FROM `olist.olist_orders` AS ord -- 주문정보 테이블
INNER JOIN tb
ON ord.order_id = tb.order_id
LEFT JOIN `olist.olist_customers` AS cust -- 고객정보 테이블
ON ord.customer_id = cust.customer_id -- join 조건 : 주문 건의 고객 id
WHERE 1 = 1
AND order_status = 'delivered'
AND EXTRACT(YEAR FROM order_approved_at) = 2017
GROUP BY 1, 2
)
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY customer_state ORDER BY base.cust_cnt DESC) AS cust_rownum,
DENSE_RANK() OVER (PARTITION BY customer_state ORDER BY base.cust_cnt DESC) AS cust_denserank,
RANK() OVER (PARTITION BY customer_state ORDER BY base.cust_cnt DESC) AS state_rank,
RANK() OVER (ORDER BY base.cust_cnt DESC) AS rank
FROM base
ORDER BY rank ;
- 요청사항 4
- 고객별 객단가 (고객별 평균 금액)
- 건 당 주문 금액 = 매출 / 고객 수
```sql
/* 요청사항 4. 고객 객단가 */
-- 월 기준
-- SAFE_DIVIDE 사용
SELECT
DATE_TRUNC(DATE(ord.order_approved_at), MONTH) AS ord_month,
SUM(item.price) AS ord_amt,
COUNT(DISTINCT cust.customer_unique_id) AS cust_cnt,
SAFE_DIVIDE(SUM(item.price), COUNT(DISTINCT cust.customer_unique_id)) AS avg_amt
FROM `olist.olist_orders` AS ord
INNER JOIN `olist.olist_order_items` AS item
ON ord.order_id = item.order_id
LEFT JOIN `olist.olist_customers` AS cust
ON ord.customer_id = cust.customer_id
GROUP BY ord_month
ORDER BY ord_month ;
```
- 요청사항 5
- 월별 구입 금액에 기반해서 그룹 나누기
- 그룹별 비중의 변화 확인
- 월 - 고객 unique ID - 해당 월의 구매금액 - 그룹 순서
- 300 BRL 이상 A, 150 BRL이상 300 BRL 미만 B, 그 외 C
- CASE ~ WHEN 사용
- Ex) CASE
WHEN [조건 1] THEN [결과]
ELSE [그외 결과]
- END
- order_status = delivered, shipped
- order_approved_at IS NOT NULL
```sql
/* 요청사항5. 그룹별 월별 구입금액 */
-- 월별 구입 금액에 기반해서 그룹 나누기
-- 그룹별 비중의 변화 확인
-- 월 - 고객 unique ID - 해당 월의 구매금액 - 그룹 순서
-- 300 BRL 이상 A, 150 BRL이상 300 BRL 미만 B, 그 외 C
-- order_status = delivered, shipped
-- order_approved_at IS NOT NULL
SELECT
DATE_TRUNC(DATE(ord.order_approved_at), MONTH) AS ord_month,
cust.customer_unique_id AS cust_unique_id,
SUM(item.price) AS ord_amt,
CASE
WHEN SUM(item.price) >= 300 THEN 'A'
WHEN SUM(item.price) >= 150 THEN 'B'
ELSE 'C'
END AS level
FROM `olist.olist_orders` AS ord
INNER JOIN `olist.olist_order_items` AS item
ON ord.order_id = item.order_id
LEFT JOIN `olist.olist_customers` AS cust
ON ord.customer_id = cust.customer_id
WHERE 1=1
AND ord.order_status IN ('delivered', 'shipped')
AND ord.order_approved_at IS NOT NULL
GROUP BY 1, 2
ORDER BY ord_month, ord_amt DESC;