1. 쿼리 테스트, 왜 이렇게 어렵게 느껴졌을까?
데이터 직군을 준비하거나 이직을 고민하는 분들이라면, SQL 쿼리 테스트가 얼마나 긴장되는 관문인지 잘 아실 겁니다.
SQL 문법 자체는 어렵지 않다고 느꼈는데, 막상 테스트를 앞두고 나니 '실제로 어떤 유형의 문제가 나올까?', '어느 정도 수준까지 준비해야 할까?' 하는 질문들이 머릿속을 가득 채웠습니다.
결국 다들 하는 방법대로, 잘 알려진 SQL 연습 사이트부터 시작했습니다.
2. 연습은 했는데, 왜 결과가 안 좋았을까?
HackerRank, LeetCode, 프로그래머스, solvesql을 돌아가며 꾸준히 문제를 풀었습니다. GROUP BY, JOIN, 서브쿼리, 윈도우 함수까지 주요 개념을 골고루 다뤘고, 나름 자신감도 붙었습니다.
그런데 실제 쿼리 테스트 당일, 문제를 보자마자 멈칫했습니다.
데이터 구조가 낯설었습니다. 테이블이 여러 개였고, 어떤 테이블을 어떻게 조합해야 하는지 바로 감이 잡히지 않았습니다. 연습 때는 테이블 구조가 명확하고 문제 설명도 친절했는데, 실무형 테스트는 달랐습니다. 요구사항을 스스로 해석하고 데이터 간의 관계를 파악해서 쿼리를 짜야 했습니다.
결과는 기대보다 낮은 점수였습니다. 문법이 문제는 아니었습니다. 원인은 실무와 가까운 데이터와 심화 SQL 문제에 대한 경험 부족이었습니다.
3. ChatGPT에게 실무형 문제를 요청하는 법
다음 테스트를 준비하면서 방향을 바꿨습니다. ChatGPT에게 직접 실무형 데이터셋과 문제를 만들어달라고 요청했습니다.
던진 프롬프트는 이겁니다.
"리텐션, DAU & MAU, 퍼널 분석 등 실무와 관련된 SQL 문제를 만들어줄 수 있어?"
ChatGPT는 MySQL 8.0 기준으로 테이블 구조까지 설계해서 응답했습니다. 테이블은 세 개였습니다.
| 테이블 | 주요 컬럼 |
|---|---|
users |
user_id, signup_date, channel, country |
events |
event_id, user_id, event_time, event_name, session_id |
orders |
order_id, user_id, order_time, amount |
event_name은 app_open, view_product, add_to_cart, begin_checkout, purchase로 구성됐습니다. 실제 커머스 앱의 유저 행동 로그를 그대로 재현한 구조였습니다.
4. 실제로 받은 문제들: 이런 수준이 나왔다
총 25개의 문제를 받았습니다. 초급부터 고급까지 난이도별로 구성되어 있었습니다.
초급 ~ 중급 문제 예시
출력 컬럼:
dt, dau / 정렬: dt 오름차순
출력 컬럼:
signup_date, new_users, retained_users_d1, d1_retention_rate
고급 문제 예시
app_open → view_product → add_to_cart → purchase 순서를 시간 순서대로 만족한 유저 수를 구하세요. 각 단계의 최초 시각만 사용합니다.
처음 이 문제들을 봤을 때 솔직히 당황했습니다. 지표가 뭔지는 알겠는데, 어떤 테이블을 조합해야 하는지 바로 떠오르지 않는 그 느낌이 실제 테스트와 거의 똑같았습니다.
5. MySQL에서 바로 실행 가능한 샘플 데이터
ChatGPT에게 테이블 생성문과 샘플 데이터도 함께 만들어달라고 요청했습니다. 아래 SQL을 실행하면 바로 연습 환경이 만들어집니다.
테이블 생성 (DDL)
CREATE TABLE users (
user_id INT PRIMARY KEY,
signup_date DATE NOT NULL,
channel VARCHAR(20) NOT NULL,
country VARCHAR(20) NOT NULL
);
CREATE TABLE events (
event_id INT PRIMARY KEY,
user_id INT NOT NULL,
event_time DATETIME NOT NULL,
event_name VARCHAR(50) NOT NULL,
session_id VARCHAR(50) NOT NULL
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT NOT NULL,
order_time DATETIME NOT NULL,
amount INT NOT NULL
);
샘플 데이터 INSERT
-- users 데이터
INSERT INTO users (user_id, signup_date, channel, country) VALUES
(1, '2026-01-28', 'organic', 'KR'),
(2, '2026-01-28', 'ads', 'KR'),
(3, '2026-01-29', 'referral', 'KR'),
(4, '2026-02-01', 'organic', 'KR'),
(5, '2026-02-01', 'ads', 'KR'),
(6, '2026-02-03', 'organic', 'US'),
(7, '2026-02-05', 'referral', 'KR'),
(8, '2026-02-10', 'ads', 'JP'),
(9, '2026-02-15', 'organic', 'KR'),
(10, '2026-02-20', 'ads', 'KR');
-- events 데이터
INSERT INTO events (event_id, user_id, event_time, event_name, session_id) VALUES
-- user 1: 풀 퍼널 완주 (app_open → purchase)
(1, 1, '2026-02-01 09:00:00', 'app_open', 's1'),
(2, 1, '2026-02-01 09:02:00', 'view_product', 's1'),
(3, 1, '2026-02-01 09:05:00', 'add_to_cart', 's1'),
(4, 1, '2026-02-01 09:07:00', 'purchase', 's1'),
(5, 1, '2026-02-02 10:00:00', 'app_open', 's2'),
(6, 1, '2026-02-03 11:00:00', 'app_open', 's3'),
-- user 2
(7, 2, '2026-02-01 13:00:00', 'app_open', 's4'),
(8, 2, '2026-02-01 13:03:00', 'view_product', 's4'),
(9, 2, '2026-02-02 14:00:00', 'app_open', 's5'),
-- user 3
(10, 3, '2026-02-02 08:00:00', 'app_open', 's6'),
(11, 3, '2026-02-02 08:05:00', 'view_product', 's6'),
(12, 3, '2026-02-04 08:00:00', 'app_open', 's7'),
(13, 3, '2026-02-04 08:10:00', 'begin_checkout', 's7'),
-- user 4
(14, 4, '2026-02-01 07:00:00', 'app_open', 's8'),
(15, 4, '2026-02-01 07:05:00', 'view_product', 's8'),
(16, 4, '2026-02-01 07:07:00', 'add_to_cart', 's8'),
(17, 4, '2026-02-03 12:00:00', 'app_open', 's9'),
-- user 5: 풀 퍼널 완주
(18, 5, '2026-02-01 21:00:00', 'app_open', 's10'),
(19, 5, '2026-02-08 21:00:00', 'app_open', 's11'),
(20, 5, '2026-02-08 21:05:00', 'view_product', 's11'),
(21, 5, '2026-02-08 21:06:00', 'add_to_cart', 's11'),
(22, 5, '2026-02-08 21:08:00', 'purchase', 's11'),
-- user 6
(23, 6, '2026-02-03 15:00:00', 'app_open', 's12'),
(24, 6, '2026-02-03 15:10:00', 'view_product', 's12'),
(25, 6, '2026-02-10 16:00:00', 'app_open', 's13'),
-- user 7
(26, 7, '2026-02-05 09:00:00', 'app_open', 's14'),
(27, 7, '2026-02-05 09:01:00', 'view_product', 's14'),
(28, 7, '2026-02-05 09:02:00', 'add_to_cart', 's14'),
(29, 7, '2026-02-05 09:04:00', 'begin_checkout', 's14'),
(30, 7, '2026-02-06 10:00:00', 'app_open', 's15'),
-- user 8
(31, 8, '2026-02-10 20:00:00', 'app_open', 's16'),
(32, 8, '2026-02-10 20:03:00', 'view_product', 's16'),
(33, 8, '2026-02-11 20:00:00', 'app_open', 's17'),
(34, 8, '2026-02-11 20:05:00', 'purchase', 's17'), -- add_to_cart 없이 purchase → 순차퍼널 탈락
-- user 9
(35, 9, '2026-02-15 18:00:00', 'app_open', 's18'),
(36, 9, '2026-02-15 18:05:00', 'view_product', 's18'),
(37, 9, '2026-02-16 18:00:00', 'app_open', 's19'),
-- user 10
(38, 10, '2026-02-20 10:00:00', 'app_open', 's20'),
(39, 10, '2026-02-20 10:03:00', 'view_product', 's20'),
(40, 10, '2026-02-20 10:05:00', 'add_to_cart', 's20'),
(41, 10, '2026-02-20 10:06:00', 'begin_checkout', 's20'),
(42, 10, '2026-02-21 10:00:00', 'app_open', 's21');
-- orders 데이터
INSERT INTO orders (order_id, user_id, order_time, amount) VALUES
(1, 1, '2026-02-01 09:07:00', 32000),
(2, 5, '2026-02-08 21:08:00', 54000),
(3, 8, '2026-02-11 20:05:00', 21000),
(4, 1, '2026-02-15 12:00:00', 18000),
(5, 5, '2026-02-20 22:00:00', 47000);
한 가지 포인트를 짚자면, user 8은 purchase 이벤트는 있지만 add_to_cart가 없습니다. 단순 집계에서는 구매자로 카운트되지만, 순차 퍼널 분석에서는 앞 단계를 거치지 않았으므로 탈락합니다. 이런 디테일이 실무형 문제를 까다롭게 만드는 부분입니다.
6. AI에게 내 쿼리를 피드백 받는 방법
처음에는 하나의 채팅창에서 모든 문제의 피드백을 이어받았습니다. 대화가 길어질수록 이전에 받은 피드백을 다시 찾기가 어려웠고, 문항마다 맥락이 달라 정리도 잘 되지 않았습니다.
그래서 ChatGPT에서 "SQL 실무 쿼리"라는 프로젝트를 만들고, 소스에 샘플 데이터 파일을 업로드했습니다. 프로젝트 내 모든 채팅에서 테이블 구조를 공통으로 참고하기 때문에, 매번 구조를 다시 설명할 필요가 없었습니다.
그리고 문항마다 새로운 채팅을 열어 피드백을 받았습니다. 나중에 다시 찾아보기도 쉽고, 문제별 질문과 답변이 한 곳에 모여 깔끔하게 정리됐습니다.
실제로 받은 피드백은 이런 식이었습니다.
- "서브쿼리 대신 CTE(WITH 절)를 사용하면 가독성이 좋아집니다"
- "
LEFT JOIN보다INNER JOIN이 의도에 더 맞는 표현입니다" - "윈도우 함수의
PARTITION BY기준을 확인해보세요. 현재 쿼리는 전체 기간 기준으로 순위를 매기고 있습니다"
"이 쿼리 맞아?"보다 "왜 이 방법이 더 나은지 설명해줘"라고 물으면 훨씬 깊이 있는 피드백을 받을 수 있습니다. SQL 실력은 '왜 이렇게 쓰는가'를 이해하는 데서 빠르게 늘기 때문입니다.
7. 결과는 어땠을까?
두 번째 테스트 결과는 만족스러웠습니다.
테이블 구조를 봤을 때 당황하지 않았습니다. "이전에 연습했던 커머스 이벤트 로그 구조와 비슷하다"는 느낌이 들었고, 접근 방향이 빠르게 잡혔습니다. 리텐션과 퍼널 쿼리도 여러 번 직접 써봤던 유형이라 침착하게 풀 수 있었습니다.
AI가 모든 걸 해결해 주지는 않습니다. 쿼리를 직접 짜는 건 결국 본인이고, 논리적 사고도 스스로 훈련해야 합니다. 하지만 실무에 가까운 환경을 만들고 즉각적인 피드백을 받는다는 점에서 AI는 효과적인 학습 파트너였습니다.
8. 마무리: AI는 SQL 공부의 좋은 파트너다
HackerRank나 LeetCode는 여전히 유효한 도구입니다. SQL 기초와 문법 감각을 키우는 데는 충분합니다. 하지만 실무형 쿼리 테스트를 앞두고 있다면, 그것만으로는 부족할 수 있습니다.
생성형 AI를 활용해 리텐션, DAU/MAU, 퍼널 분석 같은 실무 지표 중심의 문제를 직접 만들고, 쿼리에 대해 즉각적인 피드백을 받고 개선하는 루프를 만들어보세요. 연습의 질이 달라집니다.
실무 SQL은 문법을 아는 것과 실제로 쓰는 것 사이에 생각보다 큰 간격이 있습니다. AI는 그 간격을 좁히는 데 쓸 만한 도구입니다.
다음 쿼리 테스트를 준비 중이신 분들께 도움이 됐으면 좋겠습니다.
'with AI' 카테고리의 다른 글
| 생성형 AI를 이용한 워크플로우 개선 (0) | 2025.10.30 |
|---|