데이터 리터러시의 시작 — SQL로 직접 데이터를 꺼내고, 분석하고, 시각화하는 실무 교육자료
데이터 수집 → 전처리 → 가공 → 분석 → 시각화 → 인사이트 도출
데이터베이스는 **행(Row)**과 **열(Column)**로 구성된 테이블 형태로 데이터를 저장한다.
| 용어 | 설명 | 엑셀 비유 |
|---|---|---|
| 데이터베이스 | 데이터를 저장하는 전체 공간 | 엑셀 파일 |
| 테이블 | 특정 주제의 데이터 집합 | 시트(Sheet) |
| 행(Row) | 하나의 레코드(데이터 한 건) | 가로 한 줄 |
| 열(Column) | 데이터의 속성(필드) | 세로 한 줄 |
| 종류 | 설명 | 대표 명령어 |
|---|---|---|
| DML (데이터 조작어) | 데이터를 검색·추가·수정·삭제 | SELECT, INSERT, UPDATE, DELETE |
| DDL (데이터 정의어) | 테이블 구조를 생성·변경·삭제 | CREATE, ALTER, DROP |
| DCL (데이터 제어어) | 접근 권한을 관리 | GRANT, REVOKE |
마케터가 가장 많이 사용하는 것은 DML의 SELECT (데이터 조회)
- 구문 간 띄어쓰기 필수
- 대소문자 구분 없음 (관례상 키워드는 대문자)
- 별칭(AS)에 띄어쓰기 불가 → 언더바(
_) 사용 - 문자열·날짜 값은 작은따옴표(
') 감싸기
SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
이 순서를 정확히 지켜야 오류가 발생하지 않는다.
-- 모든 컬럼 검색
SELECT * FROM 테이블명;
-- 특정 컬럼만 검색
SELECT order_date, order_id, order_mode
FROM 테이블명;컬럼명이나 테이블명을 짧고 읽기 쉽게 변경한다.
SELECT
order_date AS ord_dt, -- 컬럼 별칭
order_id,
order_mode
FROM oe_orders AS t1; -- 테이블 별칭엑셀의 '필터'와 같은 역할. 원본 테이블에서 원하는 데이터만 추출한다.
| 연산자 | 사용법 | 설명 |
|---|---|---|
= / != |
WHERE status = 'active' |
같다 / 같지 않다 |
> / >= / < / <= |
WHERE price >= 10000 |
비교 연산 |
AND |
WHERE A = 1 AND B = 2 |
모든 조건 충족 |
OR |
WHERE A = 1 OR B = 2 |
하나라도 충족 |
BETWEEN A AND B |
WHERE idx BETWEEN 3000 AND 4000 |
A 이상 B 이하 |
IN (리스트) |
WHERE idx IN ('12535', '4000') |
여러 값 중 일치 |
LIKE |
WHERE grade LIKE '%마케팅%' |
패턴 매칭 (와일드카드) |
IS NULL |
WHERE hotel_idx IS NULL |
값이 비어있음 |
IS NOT NULL |
WHERE hotel_idx IS NOT NULL |
값이 존재함 |
| 패턴 | 의미 | 예시 |
|---|---|---|
%keyword% |
keyword를 포함하는 | LIKE '%마케팅%' |
keyword% |
keyword로 시작하는 | LIKE '서울%' |
%keyword |
keyword로 끝나는 | LIKE '%1' |
_ |
한 글자 대체 | LIKE '_A' → 2글자, A로 끝남 |
NULL은 숫자 0이 아니다. 데이터 자체가 없는 상태이므로
= NULL로 비교할 수 없고, 반드시IS NULL을 사용해야 한다.
-- 여러 조건을 괄호로 묶어 로직 순서를 명확하게
SELECT *
FROM orders
WHERE (region = '서울' OR region = '경기')
AND order_date >= '2024-01-01';데이터를 특정 컬럼 기준으로 묶어 그룹화하고, 집계 함수로 연산한다.
| 함수 | 설명 | NULL 처리 |
|---|---|---|
COUNT() |
행의 개수 | NULL 포함 |
SUM() |
합계 | NULL 제외 |
AVG() |
평균 | NULL 제외 (주의!) |
MAX() |
최댓값 | NULL 제외 |
MIN() |
최솟값 | NULL 제외 |
AVG 주의: NULL을 제외하고 평균을 구하므로, NULL을 0으로 포함해서 평균내려면 별도 처리 필요
SELECT
category,
COUNT(*) AS order_cnt,
SUM(ord_price) AS total_price,
AVG(ord_price) AS avg_price
FROM orders
WHERE region = '서울특별시'
GROUP BY category;GROUP BY 이후 집계된 결과에 조건을 걸 때 사용한다.
WHERE vs HAVING: WHERE는 원본 테이블 필터링, HAVING은 집계 결과 필터링. WHERE 안에는
SUM() > 100같은 집계 함수 조건을 쓸 수 없다.
SELECT
category,
SUM(ord_price) AS total_price
FROM orders
GROUP BY category
HAVING SUM(ord_price) > 100000; -- 집계 결과에 조건 적용| 옵션 | 의미 | 비고 |
|---|---|---|
ASC |
오름차순 (작은 → 큰) | 기본값, 생략 가능 |
DESC |
내림차순 (큰 → 작은) | 명시 필수 |
SELECT category, SUM(ord_price) AS total_price
FROM orders
GROUP BY category
ORDER BY total_price DESC; -- 금액 큰 순으로 정렬
-- 숫자로 간편하게 지정 가능 (SELECT의 n번째 컬럼)
ORDER BY 2 DESC;
-- 다중 정렬
ORDER BY 1 ASC, 2 DESC;SELECT
month,
category,
SUM(ord_price) AS total_price
FROM reservation_rec_single -- 1. 대상 테이블
WHERE order_date >= '2020-01-01' -- 2. 원본 필터링
GROUP BY month, category -- 3. 그룹화
HAVING SUM(ord_price) > 100 -- 4. 집계 결과 필터링
ORDER BY 1; -- 5. 정렬조건에 따라 데이터를 분류하거나 새로운 값을 부여한다. 엑셀의 IF 함수와 유사.
SELECT
customer_id,
CASE
WHEN age < 10 THEN '10대 미만'
WHEN age BETWEEN 10 AND 19 THEN '10대'
WHEN age BETWEEN 20 AND 29 THEN '20대'
WHEN age BETWEEN 30 AND 39 THEN '30대'
WHEN age BETWEEN 40 AND 49 THEN '40대'
ELSE '50대 이상'
END AS age_group
FROM customers;컬럼의 데이터 타입을 변경한다.
| 변환 타입 | 설명 |
|---|---|
STRING |
문자열 |
DATE |
날짜 |
INT64 |
정수 |
FLOAT64 |
실수 |
SELECT CAST(order_date AS STRING) AS date_str
FROM orders;| 함수 | 설명 | 예시 |
|---|---|---|
LEFT(col, n) |
왼쪽부터 n글자 추출 | LEFT('2024-03-15', 4) → '2024' |
RIGHT(col, n) |
오른쪽부터 n글자 추출 | RIGHT('2024-03-15', 2) → '15' |
SUBSTR(col, start, n) |
start 위치부터 n글자 추출 | SUBSTR('2024-03-15', 6, 2) → '03' |
-- 날짜에서 연도만 추출
SELECT SUBSTR(CAST(order_date AS STRING), 1, 4) AS yyyy
FROM orders;행(Row) 간 관계를 연산. 과거/미래 행의 값을 현재 행으로 가져온다.
| 함수 | 설명 |
|---|---|
LAG(col, n) OVER (ORDER BY ...) |
n번째 이전 행의 값 |
LEAD(col, n) OVER (ORDER BY ...) |
n번째 이후 행의 값 |
-- 전월 매출과 당월 매출을 나란히 보기
SELECT
month,
revenue,
LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue
FROM monthly_sales;복잡한 쿼리에서 동일한 블록을 반복하지 않도록 임시 테이블을 생성한다. 쿼리 실행 시에만 존재하고 사라진다.
-- 단일 WITH
WITH total_order AS (
SELECT order_type, order_id
FROM oe_orders
)
SELECT order_type, COUNT(order_id) AS cnt
FROM total_order
GROUP BY order_type;
-- 다중 WITH (콤마로 연결)
WITH
table_a AS (SELECT ...),
table_b AS (SELECT ...)
SELECT *
FROM table_a
JOIN table_b ON ...;두 개 이상의 테이블을 **공통 키(Primary Key)**를 기준으로 가로로 병합한다.
| JOIN 유형 | 결과 |
|---|---|
| INNER JOIN | 양쪽 테이블에 공통으로 존재하는 값만 |
| LEFT JOIN | 왼쪽 테이블 전체 + 오른쪽에서 매칭되는 값 |
| RIGHT JOIN | 오른쪽 테이블 전체 + 왼쪽에서 매칭되는 값 |
| FULL JOIN | 양쪽 테이블의 모든 데이터 |
INNER JOIN: LEFT JOIN: FULL JOIN:
┌───┐ ┌───┐ ┌───┐
│ ∩ │ │ A ∪∩│ │A ∪ B│
└───┘ └───┘ └───┘
-- LEFT JOIN 예시
SELECT
t1.order_id,
t1.customer_id,
t2.customer_name
FROM orders AS t1
LEFT JOIN customers AS t2
ON t1.customer_id = t2.customer_id;주의 (VLOOKUP과의 차이): JOIN은 1:N 매칭이 가능하므로, 데이터가 중복 뻥튀기될 수 있다. 거래액 등 합계를 구할 때 반드시 중복 여부를 확인할 것!
JOIN이 가로 연결이라면, UNION은 세로 연결이다.
| 연산자 | 설명 |
|---|---|
UNION ALL |
모든 결과를 그대로 합침 (중복 허용) |
UNION |
중복 제거 후 합침 |
SELECT channel, date, ad_cost FROM campaign_a
UNION ALL
SELECT channel, date, ad_cost FROM campaign_b;규칙: 위아래 쿼리의 컬럼 타입, 순서, 개수가 모두 동일해야 한다. 컬럼명은 달라도 되며, 결과는 첫 번째 쿼리의 컬럼명을 따른다.
- 분석 결과를 한눈에 파악하게 하고 스토리를 효과적으로 전달
- 전달하고 싶은 부분을 강조하고 분석가의 의도를 담아냄
- 잘못된 시각화는 인지적 오류를 유발할 수 있으므로 주의
| 분석 목적 | 추천 차트 | 예시 |
|---|---|---|
| 구성 비율 비교 | 파이/도넛, 100% 누적 열 | 카테고리별 매출 비중 |
| 항목 간 크기 비교 | 막대(Bar)/열 그래프 | 채널별 주문 수 비교 |
| 시간 추이/트렌드 | 선(Line) 그래프 | 월별 매출 추이 |
| 크기 + 추이 동시 | 콤보 차트 (막대 + 선) | 월별 매출(막대) + 전환율(선) |
- SQL 쿼리 결과를 스프레드시트로 내보내기
- 데이터 범위 선택 →
삽입 > 차트 - 차트 편집기에서 X축/Y축 설정
- 콤보 차트, 데이터 라벨, 색상 등 세부 조정
- 피벗 테이블:
데이터 > 피벗 테이블로 요약 분석 (SQL의 GROUP BY와 유사한 역할)
시간의 흐름에 따라 데이터를 순서대로 나열하여 트렌드를 모니터링하는 분석 기법.
핵심 가정: 과거의 트렌드가 미래에도 지속된다고 가정하고, 현재를 분석하며 미래를 예측한다.
| 유형 | 설명 | 예시 |
|---|---|---|
| 규칙적 변동성 | 장기 추세, 계절 순환 | 여름 성수기 매출 상승 |
| 불규칙 변동성 | 외부 요인에 의한 일시적 변화 | 프로모션, 이벤트, 코로나 |
SELECT
EXTRACT(YEAR FROM dt) AS year,
EXTRACT(MONTH FROM dt) AS month,
SUM(purchase_amount) AS amount
FROM total_purchase_log
GROUP BY 1, 2
ORDER BY 1, 2;WITH 절 + CASE WHEN으로 연도별 매출을 가로 전개(Pivot)한 뒤 증감률을 계산한다.
WITH order_raw AS (
SELECT
EXTRACT(YEAR FROM dt) AS year,
EXTRACT(MONTH FROM dt) AS month,
SUM(purchase_amount) AS amount
FROM total_purchase_log
GROUP BY 1, 2
)
SELECT
month,
SUM(CASE WHEN CAST(year AS STRING) = '2023' THEN amount END) AS amount_2023,
SUM(CASE WHEN CAST(year AS STRING) = '2024' THEN amount END) AS amount_2024,
ROUND(
SUM(CASE WHEN CAST(year AS STRING) = '2024' THEN amount END)
/ SUM(CASE WHEN CAST(year AS STRING) = '2023' THEN amount END)
* 100
) - 100 AS yoy_growth_rate
FROM order_raw
GROUP BY 1
ORDER BY 1;CASE WHEN으로 고객을 연령대/등급/행동별로 분류하여 세그먼트 분석
SELECT
CASE
WHEN age BETWEEN 20 AND 29 THEN '20대'
WHEN age BETWEEN 30 AND 39 THEN '30대'
ELSE '기타'
END AS age_group,
gender,
COUNT(*) AS customer_cnt,
SUM(order_amount) AS total_amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY 1, 2
ORDER BY 4 DESC;JOIN + 집계로 일자별 광고 효율(광고당 주문수) 산출
SELECT
a.date,
COUNT(DISTINCT a.ad_id) AS ad_cnt,
COUNT(DISTINCT o.order_id) AS order_cnt,
ROUND(COUNT(DISTINCT o.order_id) / COUNT(DISTINCT a.ad_id), 2) AS orders_per_ad
FROM ads a
LEFT JOIN orders o ON a.date = o.order_date AND a.channel = o.channel
WHERE a.ad_id IN (2, 3, 4, 7)
GROUP BY 1
HAVING orders_per_ad >= 2.5
ORDER BY 1;UNION ALL로 여러 캠페인 테이블을 세로 합치기
SELECT channel, date, ad_cost, 'BM' AS campaign_type FROM bm_mkt_promo
UNION ALL
SELECT channel, date, ad_cost, 'BR' AS campaign_type FROM br_mkt_promo
ORDER BY date;특정 가입월(M+0) 기준으로 N개월 후 잔존 고객을 추적
WITH cohort AS (
SELECT
customer_id,
MIN(EXTRACT(MONTH FROM first_order_date)) AS cohort_month
FROM customers
GROUP BY 1
)
SELECT
c.cohort_month,
EXTRACT(MONTH FROM o.order_date) - c.cohort_month AS months_after,
COUNT(DISTINCT o.customer_id) AS retained_customers
FROM cohort c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY 1, 2
ORDER BY 1, 2;시각화 팁: 코호트 결과를 스프레드시트 피벗 테이블로 전환한 뒤, 조건부 서식(색상 스케일)을 적용하면 잔존율 패턴이 한눈에 보인다.
SELECT
EXTRACT(HOUR FROM order_datetime) AS hour,
COUNT(*) AS order_cnt,
SUM(order_amount) AS total_amount
FROM orders
GROUP BY 1
ORDER BY 1;SELECT (5) 조회할 컬럼 선택
FROM (1) 대상 테이블 지정
WHERE (2) 원본 데이터 필터링
GROUP BY (3) 그룹화
HAVING (4) 집계 결과 필터링
ORDER BY (6) 정렬
괄호 안 숫자가 실행 순서. 작성 순서와 실행 순서가 다르다!
| 기능 | 문법 | 설명 |
|---|---|---|
| 조회 | SELECT col FROM table |
기본 데이터 조회 |
| 별칭 | col AS alias |
컬럼/테이블 이름 변경 |
| 필터 | WHERE condition |
원본 데이터 조건 필터 |
| 그룹화 | GROUP BY col |
컬럼 기준 묶음 |
| 집계 필터 | HAVING agg() > n |
집계 결과 조건 필터 |
| 정렬 | ORDER BY col DESC |
오름차순/내림차순 |
| 조건 분기 | CASE WHEN ... THEN ... END |
IF와 유사한 조건 분류 |
| 형변환 | CAST(col AS type) |
데이터 타입 변경 |
| 문자열 추출 | SUBSTR(col, start, len) |
부분 문자열 추출 |
| 윈도우 | LAG(col, n) OVER (ORDER BY ...) |
이전/이후 행 참조 |
| 임시 테이블 | WITH name AS (SELECT ...) |
재사용 가능한 쿼리 블록 |
| 가로 연결 | JOIN ... ON key |
테이블 병합 |
| 세로 연결 | UNION ALL |
쿼리 결과 세로 합치기 |
| 연산자 | 문법 | 의미 |
|---|---|---|
= / != |
col = 'value' |
같다 / 다르다 |
> >= < <= |
col >= 100 |
비교 |
AND / OR |
A AND B |
그리고 / 또는 |
BETWEEN |
col BETWEEN A AND B |
A~B 범위 |
IN |
col IN ('a', 'b') |
목록 내 일치 |
LIKE |
col LIKE '%keyword%' |
패턴 매칭 |
IS NULL |
col IS NULL |
값 없음 |
IS NOT NULL |
col IS NOT NULL |
값 있음 |
🔥 7일 한정 특가:
99,000원→ 29,000원