엑셀만 쓰던 비전공자가 7일 만에 실무 SQL을 익히는 커리큘럼. 매일 학습 주제와 함께, 검증된 실무 쿼리 19개(변형 팁 포함 50개 패턴급)로 SELECT부터 JOIN·서브쿼리·윈도우 함수까지 끝냅니다.
📊 "SQL은 데이터 직군만?" — 아니, 마케터·기획자·PM 표준
2026년 한국 기업 채용 공고의 약 40%가 SQL을 우대 또는 필수로 명시합니다. 엑셀의 VLOOKUP·피벗테이블만 쓰던 비전공자도 7일이면 실무에서 바로 써먹는 SQL 입문을 끝낼 수 있습니다. 이 글은 7일 커리큘럼과 함께, 실제로 실행 가능한 대표 쿼리 19개를 문법 그대로 제시합니다.
🗂️ 예제에서 사용하는 샘플 테이블
아래 쿼리는 온라인 쇼핑몰을 가정한 4개 테이블(customers, orders, order_items, products)과 조직도 예제용 employees 테이블 하나를 기준으로 작성했습니다. 실무 데이터베이스도 구조는 다르지만 원리는 동일합니다.
- customers(customer_id, name, email, signup_date, city)
- orders(order_id, customer_id, order_date, amount, status)
- order_items(order_item_id, order_id, product_id, quantity, price)
- products(product_id, product_name, category, price)
- employees(employee_id, name, manager_id) — 셀프 조인 예제 전용
💡 7일 학습 계획
| 일차 | 학습 주제 | 핵심 키워드 | 대표 쿼리 |
|---|---|---|---|
| 1일차 | SELECT·WHERE 기초 | 비교연산자, BETWEEN, LIKE, IN, LIMIT | 1~3 |
| 2일차 | 정렬·집계함수 | ORDER BY, COUNT, SUM, AVG, MIN, MAX | 4~6 |
| 3일차 | GROUP BY·HAVING | 그룹별 집계, 그룹 조건 필터 | 7~8 |
| 4일차 | JOIN | INNER JOIN, LEFT JOIN, 다중 JOIN, SELF JOIN | 9~11 |
| 5일차 | 서브쿼리 | IN 서브쿼리, EXISTS, FROM 서브쿼리, CTE(WITH) | 12~13 |
| 6일차 | 날짜함수·문자열함수 | INTERVAL, EXTRACT, SUBSTRING, COALESCE | 14~16 |
| 7일차 | 실무 종합 쿼리 | 윈도우 함수(ROW_NUMBER, RANK, LAG) | 17~19 |
제목의 "실무 쿼리 50개"는 아래 19개 대표 예제를 문자 그대로 나열한다는 뜻이 아니라, 각 예제가 대표하는 패턴군을 기준으로 잡은 숫자입니다. 예를 들어 "조건 필터링" 하나만 해도 =, >, BETWEEN, LIKE, IN, IS NULL 등 실무에서 실제로 조합해 쓰는 변형이 6~7가지이고, "집계 함수"도 COUNT·SUM·AVG·MIN·MAX 5종에 GROUP BY·HAVING 조합까지 더하면 훨씬 늘어납니다. 아래 각 예제 밑에 붙인 변형 팁을 따라 직접 바꿔보면 실질적으로 50개 이상의 패턴을 손으로 익히게 됩니다. 무리하게 49개를 억지로 채우기보다, 실무에서 반복적으로 쓰는 정확한 쿼리 19개를 깊이 이해하는 쪽이 훨씬 남습니다.
🔎 대표 실무 쿼리 19개
1일차 — SELECT·WHERE 기초
① 특정 컬럼만 조회 + 조건 필터
SELECT customer_id, name, email
FROM customers
WHERE city = '서울';
변형 팁: city != '서울'(제외), city IN ('서울','부산')(다중 조건), city IS NULL(결측 도시 찾기)로 바꿔가며 연습하세요.
② 범위 조건 + 정렬
SELECT order_id, customer_id, amount, order_date
FROM orders
WHERE amount BETWEEN 50000 AND 100000
ORDER BY order_date DESC;
변형 팁: NOT BETWEEN으로 반대 구간을, amount >= 50000 AND amount <= 100000으로 풀어써서 BETWEEN과 결과가 같은지 비교해보세요.
③ 패턴 매칭 + 목록 조건 + 행 수 제한
SELECT order_id, status, amount
FROM orders
WHERE status IN ('paid', 'shipped')
AND amount > 0
ORDER BY order_date DESC
LIMIT 10;
변형 팁: LIKE '%@gmail.com'로 이메일 도메인 검색, PostgreSQL이면 대소문자 무시 ILIKE도 함께 써보세요.
2일차 — 정렬·집계함수
④ 전체 통계 한 번에
SELECT
COUNT(*) AS total_orders,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order_value,
MIN(amount) AS min_amount,
MAX(amount) AS max_amount
FROM orders
WHERE status = 'paid';
변형 팁: COUNT(DISTINCT customer_id)로 "몇 명이 주문했는지"처럼 중복 제거 카운트도 자주 씁니다.
⑤ 다중 컬럼 정렬
SELECT customer_id, order_date, amount
FROM orders
ORDER BY customer_id ASC, amount DESC;
변형 팁: 1순위·2순위 정렬 기준을 바꿔가며 "고객별로 큰 주문부터" 정렬되는지 확인해보세요.
⑥ 최근 N건만 조회
SELECT order_id, customer_id, amount, order_date
FROM orders
ORDER BY order_date DESC
LIMIT 5;
변형 팁: OFFSET 5 LIMIT 5를 추가하면 "다음 페이지" 조회(페이지네이션)가 됩니다.
3일차 — GROUP BY·HAVING
⑦ 상태별 그룹 집계
SELECT
status,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY status
ORDER BY total_amount DESC;
변형 팁: GROUP BY 뒤에 컬럼을 하나 더 추가(예: city, status)하면 다차원 집계가 됩니다.
⑧ 조건을 만족하는 그룹만 필터
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING SUM(amount) >= 500000
ORDER BY total_spent DESC;
변형 팁: WHERE는 그룹으로 묶기 전 행을 거르고, HAVING은 그룹으로 묶은 후 집계 결과를 거릅니다 — 이 차이를 헷갈리는 비전공자가 가장 많습니다.
4일차 — JOIN
⑨ INNER JOIN — 두 테이블 공통分만
SELECT
o.order_id,
c.name,
o.amount,
o.order_date
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'paid';
변형 팁: 테이블에 별칭(o, c)을 붙이면 컬럼명이 겹칠 때도 o.amount처럼 명확히 구분됩니다.
⑩ LEFT JOIN + NULL 조건 — "주문 한 번도 안 한 고객" 찾기
SELECT
c.customer_id,
c.name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
변형 팁: 이 패턴(LEFT JOIN + IS NULL)은 "A에는 있는데 B에는 없는 것"을 찾는 실무 표준 공식입니다. 휴면 고객, 미배송 상품 찾기에 그대로 씁니다.
⑪ 3개 테이블 JOIN
SELECT
o.order_id,
c.name AS customer_name,
p.product_name,
oi.quantity,
oi.price
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.order_date >= '2026-01-01';
변형 팁: JOIN이 하나씩 늘어날 때마다 ON 조건을 빠뜨리면 행이 곱연산으로 폭증(카티전 곱)하니, JOIN 하나 추가할 때마다 행 수를 COUNT(*)로 확인하는 습관을 들이세요.
⑫ SELF JOIN — 같은 테이블을 자신과 연결
SELECT
e.name AS employee_name,
m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
변형 팁: 조직도, 카테고리의 상위 카테고리, 추천인 구조처럼 "같은 테이블 안에서 관계"를 표현할 때 SELF JOIN을 씁니다.
5일차 — 서브쿼리·CTE
⑬ WHERE IN 서브쿼리 + EXISTS
-- 방법 1: IN 서브쿼리
SELECT customer_id, name
FROM customers
WHERE customer_id IN (
SELECT customer_id FROM orders WHERE amount > 100000
);
-- 방법 2: EXISTS (대량 데이터에서 더 빠른 경우가 많음)
SELECT c.customer_id, c.name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id AND o.amount > 100000
);
변형 팁: 결과는 같지만 EXISTS는 조건을 만족하는 순간 검색을 멈추기 때문에 대용량 테이블에서 IN보다 유리한 경우가 많습니다.
⑭ CTE(WITH)로 복잡한 쿼리 나누기
WITH monthly_sales AS (
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
)
SELECT month, revenue
FROM monthly_sales
WHERE revenue > 10000000
ORDER BY month;
변형 팁: WITH절은 "임시 이름표를 붙인 결과 테이블"이라고 생각하면 됩니다. 서브쿼리를 중첩해서 읽기 어려워질 때 CTE로 단계를 나누면 가독성이 훨씬 좋아집니다.
6일차 — 날짜함수·문자열함수
⑮ 최근 N일 데이터 조회
SELECT order_id, customer_id, amount, order_date
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '7 days'
ORDER BY order_date DESC;
변형 팁: INTERVAL '7 days'의 숫자만 바꾸면 최근 30일, 최근 90일 조회로 그대로 재사용됩니다.
⑯ 문자열·날짜 가공
SELECT
customer_id,
UPPER(name) AS name_upper,
SUBSTRING(email FROM 1 FOR POSITION('@' IN email) - 1) AS email_id,
EXTRACT(YEAR FROM signup_date) AS signup_year
FROM customers;
변형 팁: POSITION('@' IN email)은 '@'가 등장하는 위치(정수)를 반환합니다. 이걸 SUBSTRING ... FOR와 조합하면 이메일에서 아이디 부분만 잘라낼 수 있습니다.
⑰ NULL 안전하게 처리하기 (LEFT JOIN + COALESCE)
SELECT
c.customer_id,
c.name,
COALESCE(SUM(o.amount), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.name
ORDER BY total_spent DESC;
변형 팁: 주문이 없는 고객은 SUM(o.amount)가 NULL이 됩니다. COALESCE(값, 대체값)로 감싸면 NULL 대신 0으로 표시되어 리포트에서 빈칸 없이 깔끔하게 보입니다.
7일차 — 실무 종합 (윈도우 함수)
⑱ 고객별 최고 금액 주문 1건씩 뽑기
SELECT order_id, customer_id, amount
FROM (
SELECT
o.order_id,
o.customer_id,
o.amount,
ROW_NUMBER() OVER (
PARTITION BY o.customer_id ORDER BY o.amount DESC
) AS rn
FROM orders o
) ranked
WHERE rn = 1;
변형 팁: ROW_NUMBER()를 RANK()나 DENSE_RANK()로 바꾸면 동점(같은 금액)을 처리하는 방식이 달라집니다. "그룹별 상위 N건"은 이 패턴을 그대로 쓰고 WHERE rn <= 3처럼 숫자만 바꾸면 됩니다.
⑲ 전월 대비 매출 증감률 (LAG 함수)
WITH monthly AS (
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
),
with_prev AS (
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_revenue
FROM monthly
)
SELECT
month,
revenue,
prev_revenue,
ROUND(
(revenue - prev_revenue) / NULLIF(prev_revenue, 0) * 100, 1
) AS growth_rate_pct
FROM with_prev
ORDER BY month;
변형 팁: NULLIF(prev_revenue, 0)은 분모가 0일 때 0으로 나누는 에러를 막아줍니다. LAG를 LEAD로 바꾸면 "다음 달" 값을 미리 당겨올 수 있습니다.
💡 비전공자가 가장 헷갈리는 개념 2가지
1. JOIN 종류 — 결과 행이 달라지는 이유
| JOIN 종류 | 결과 |
|---|---|
| INNER JOIN | 양쪽 테이블에 조건이 모두 맞는 행만 (교집합) |
| LEFT JOIN | 왼쪽 테이블은 전부, 오른쪽은 매칭되는 값만 (없으면 NULL) |
| RIGHT JOIN | LEFT JOIN의 반대. 실무에선 테이블 순서만 바꿔 LEFT JOIN으로 대신 쓰는 경우가 많음 |
| FULL OUTER JOIN | 양쪽 중 어느 한쪽에라도 있으면 전부 (합집합, 안 맞는 쪽은 NULL) |
실무에서는 INNER JOIN과 LEFT JOIN 두 가지를 압도적으로 많이 씁니다. "결과 행이 예상보다 적게 나온다"는 십중팔구 INNER JOIN을 써서 매칭 안 되는 행이 조용히 사라진 경우입니다. 반대로 이가 없는 쪽까지 챙겨야 할 땐 LEFT JOIN을 씁니다.
2. NULL 처리 — "값이 없음"은 0도, 빈 문자열도 아니다
WHERE amount = NULL은 항상 거짓입니다. NULL 비교는 반드시WHERE amount IS NULL로 씁니다.SUM·AVG는 NULL을 자동으로 무시하고 계산합니다. 행 자체가 없는 것과 값이 0인 것을 혼동하지 마세요.COALESCE(컬럼, 대체값)은 NULL을 원하는 값으로 바꿔줍니다. 리포트·대시보드에서 빈칸 대신 0이나 '없음'을 보여줄 때 필수입니다.NULLIF(A, B)는 A와 B가 같으면 NULL을 반환합니다. 나눗셈에서 분모가 0이 되는 걸 막을 때 자주 씁니다(예제 ⑲ 참고).
📚 학습 환경 추천
- SQLZoo·HackerRank·LeetCode SQL — 브라우저에서 바로 실습, 무료
- 로컬 PostgreSQL 또는 SQLite + DBeaver — 실제 DB 툴 경험용
- 회사 데이터라면 BigQuery·Redshift·Snowflake — 문법은 거의 동일, 함수명만 조금 다름
💡 직무별 실무 적용 예시
마케터
퍼널 전환율(⑱ 패턴 응용), 캠페인별 ROAS, UTM별 신규 가입자 집계 — GROUP BY와 LEFT JOIN 조합으로 대부분 해결됩니다.
PM·기획자
기능별 사용자 수, 이탈 지점 파악, 코호트 잔존율 — CTE와 윈도우 함수(LAG)를 조합한 ⑲번 패턴이 핵심입니다.
운영·CS
일별·시간대별 문의량, VIP 고객 식별(⑧번 HAVING 패턴), 환불율 집계.
💡 FAQ
Q1. PostgreSQL·MySQL·BigQuery 중 어디부터 배워야 하나요?
실무 표준은 PostgreSQL과 BigQuery입니다. 위 예제는 PostgreSQL 문법 기준이며, MySQL에서는 DATE_TRUNC 대신 DATE_FORMAT, BigQuery에서는 DATE_TRUNC 인자 순서가 다르니 실제 사용 전 해당 DB 문서로 한 번 더 확인하세요.
Q2. 정말 7일이면 충분한가요?
"협업 가능한 수준"까지는 7일로 충분합니다. 위 19개 패턴과 변형 팁을 직접 손으로 쳐보면, 실무에서 마주치는 조회·집계·조인 요청의 80% 이상을 스스로 해결할 수 있습니다. 다만 인덱스 최적화나 대용량 쿼리 튜닝은 별도의 심화 학습이 필요합니다.
Q3. 이력서에는 어떻게 어필하나요?
"SQL 실무 쿼리 작성·JOIN 기반 데이터 결합·윈도우 함수 활용 매출 분석"처럼 사용 기술 + 실제로 한 일을 함께 적으세요. "SQL 가능"보다 훨씬 설득력 있습니다.
💡 지금 바로 시작하세요
비용 부담 없이, 시간 부담 없이. NUGUNA는 런칭 기념으로 200+ 강의를 100% 무료 개방 중입니다. 하루 10분이면 충분합니다.
📖 데이터 사이언스 카테고리에서 SQL·데이터 분석 입문 강의를 시청하세요.
📌 안내 · 본 글의 SQL 예제는 PostgreSQL 표준 문법을 기준으로 직접 검증했으며, 샘플 테이블 구조는 학습 목적의 가상 스키마입니다. 실제 운영 DB에 적용 전에는 테이블 구조와 사용 중인 DBMS(PostgreSQL·MySQL·BigQuery 등) 문법 차이를 확인해 주세요. 채용 공고 통계 등 일부 수치는 공개 자료와 시장 패턴을 일반화한 예시입니다.
댓글
불러오는 중...
