1. Problem
몰을 방문했지만 거래(Transaction)를 한 건도 발생시키지 않은 방문 건수를 고객별로 집계해야 한다. 단순히 '한 번이라도 산 적 없는 고객'을 찾는 것이 아니라, **'거래가 발생하지 않은 방문 그 자체'**를 카운트하는 것이 핵심이다. 즉, 평소 물건을 잘 사던 고객이라도 특정 방문 때 구매 없이 나갔다면 그 횟수를 집계에 포함해야 한다.
2. Solution
거래 테이블과 매칭되지 않는 방문 기록을 효율적으로 필터링하기 위해 두 가지 전략을 사용한다.
[전략 A] 필터링 중심: LEFT JOIN + IS NULL (권장) Visits 테이블을 기준으로 Transactions 테이블을 붙였을 때, 거래 정보가 없어 NULL이 된 행들만 먼저 골라낸 뒤 집계한다.
[전략 B] 논리 중심: NOT IN 또는 NOT EXISTS 거래 테이블에 존재하지 않는 visit_id를 하위 쿼리에서 제외하는 방식으로, 직관적인 대칭 차집합의 논리를 가진다.
3. Takeaway (데이터의 공백이 주는 인사이트)
분석가로서 '있는 데이터'보다 '없는 데이터'를 다룰 때 범하기 쉬운 실수와 그 해결책을 정립한다.
- WHERE와 HAVING의 결정적 차이: 유진 님의 오답 사례처럼 GROUP BY 후 HAVING으로 필터링하면, 고객 54번처럼 '구매한 방문'과 '구매 안 한 방문'이 섞여 있는 경우 전체 방문이 합쳐져 0이 아닌 숫자가 되어버린다. 따라서 그룹화하기 전(WHERE)에 개별 방문 건의 성격(구매 여부)을 먼저 정의해야 한다.
- 데이터 모델링적 사고: 거래가 없는 방문을 분석하는 것은 '구매 전환 실패 원인'을 파악하는 첫걸음이다. LEFT JOIN은 두 테이블을 합치는 도구를 넘어, 두 집합 사이의 '간극'을 시각화하는 도구로 활용되어야 한다.
- COUNT 대상의 선정: 거래가 없는 건을 세는 것이므로 COUNT(t.transaction_id)가 아닌 원본 방문 기록인 COUNT(v.visit_id) 또는 COUNT(*)를 사용해야 정확한 지표 산출이 가능하다.
https://leetcode.com/problems/customer-who-visited-but-did-not-make-any-transactions/description/
Table: Visits
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| visit_id | int |
| customer_id | int |
+-------------+---------+
visit_id is the column with unique values for this table.
This table contains information about the customers who visited the mall.
Table: Transactions
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| transaction_id | int |
| visit_id | int |
| amount | int |
+----------------+---------+
transaction_id is column with unique values for this table.
This table contains information about the transactions made during the visit_id.
Write a solution to find the IDs of the users who visited without making any transactions and the number of times they made these types of visits.
Return the result table sorted in any order.
The result format is in the following example.
Example 1:
Input:
Visits
+----------+-------------+
| visit_id | customer_id |
+----------+-------------+
| 1 | 23 |
| 2 | 9 |
| 4 | 30 |
| 5 | 54 |
| 6 | 96 |
| 7 | 54 |
| 8 | 54 |
+----------+-------------+
Transactions
+----------------+----------+--------+
| transaction_id | visit_id | amount |
+----------------+----------+--------+
| 2 | 5 | 310 |
| 3 | 5 | 300 |
| 9 | 5 | 200 |
| 12 | 1 | 910 |
| 13 | 2 | 970 |
+----------------+----------+--------+
Output:
+-------------+----------------+
| customer_id | count_no_trans |
+-------------+----------------+
| 54 | 2 |
| 30 | 1 |
| 96 | 1 |
+-------------+----------------+
Explanation:
Customer with id = 23 visited the mall once and made one transaction during the visit with id = 12.
Customer with id = 9 visited the mall once and made one transaction during the visit with id = 13.
Customer with id = 30 visited the mall once and did not make any transactions.
Customer with id = 54 visited the mall three times. During 2 visits they did not make any transactions, and during one visit they made 3 transactions.
Customer with id = 96 visited the mall once and did not make any transactions.
As we can see, users with IDs 30 and 96 visited the mall one time without making any transactions. Also, user 54 visited the mall twice and did not make any transactions.
1. 문제의 요구사항을 다시 뜯어볼까요?
문제 문구:
"find the IDs of the users who visited without making any transactions and the number of times they made these types of visits." (거래를 하지 않고 방문한 유저의 ID와, **그러한 종류의 방문(거래 없는 방문)**을 몇 번 했는지 구하세요.)
즉, 출제자는 **"이 고객이 평소에 물건을 사든 말든 상관없어. 일단 '거래 없이 그냥 나간 방문' 그 자체만 다 가져와서 세어줘"**라고 말하고 있는 것입니다.
2. 예시 데이터의 '고객 54번'이 결정적 증거입니다
예시를 다시 보면 고객 54번은 총 3번 방문했습니다.
- 방문 5: 거래 있음 (3건)
- 방문 7: 거래 없음
- 방문 8: 거래 없음
사용자님의 논리대로라면: 54번 고객은 '방문 5'에서 거래를 했으므로, COUNT(t.transaction_id)는 0이 아니게 되어 결과에서 아예 사라져야 합니다.
하지만 문제의 정답(Output)을 보면: 54번 고객은 count_no_trans가 2로 찍혀서 결과에 포함되어 있습니다. 즉, **"거래가 있었던 방문 5는 무시하고, 거래가 없었던 방문 7과 8만 따로 세어줘"**라는 뜻입니다.
1. 정답 쿼리
방법1. NOT IN 또는 NOT EXISTS 활용
가장 직관적인 방법으로, 거래 테이블에 없는 visit_id만 골라내는 방식입니다.
SELECT customer_id, COUNT(visit_id) AS count_no_trans
FROM Visits
WHERE visit_id NOT IN (SELECT visit_id FROM Transactions)
GROUP BY customer_id;
방법2. WHERE 절에서 NULL을 직접 체크
WHERE는 그룹화(GROUP BY)를 하기 전에 데이터를 쳐내기 때문에, 컴퓨터가 처리해야 할 데이터 양을 미리 줄여줍니다. .
SELECT v.customer_id, COUNT(v.visit_id) AS count_no_trans
FROM Visits v
LEFT JOIN Transactions t ON v.visit_id = t.visit_id
WHERE t.transaction_id IS NULL -- 거래가 없는 행만 먼저 필터링
GROUP BY v.customer_id;
2. 나의 오답
#오답 쿼리
SELECT v.customer_id, count(v.visit_id) AS count_no_trans
FROM Visits v
LEFT JOIN Transactions t ON v.visit_id = t.visit_id
GROUP BY v.customer_id # 고객으로 묶고
HAVING COUNT(t.transaction_id) = 0; # 거래횟수 0으로 필터링하면, 54번 고객의 무거래 방문도 제거됨
🎯 쿼리테스트 출제자의 의도
이 문제는 난이도는 낮지만, 데이터 분석의 가장 중요한 기초를 묻습니다.
① JOIN의 특성 이해 (Matching vs Non-matching)
단순히 공통된 데이터를 뽑는 INNER JOIN 외에, **"연결되지 않은 데이터"**를 LEFT JOIN과 IS NULL을 통해 찾아낼 수 있는지를 봅니다.
- 의도: "데이터의 공백을 통해 인사이트(구매 미발생 원인 분석 등)를 도출할 수 있는가?"
② 집계 함수(COUNT)의 정확한 사용
거래가 없는 데이터를 셀 때 COUNT(*)를 쓸 것인지, 특정 컬럼을 쓸 것인지에 따라 결과가 달라질 수 있습니다. 그룹화된 상태에서 정확한 개수를 산출하는 능력을 평가합니다.
- 의도: "비즈니스 지표(미구매 방문 횟수)를 오류 없이 산출할 수 있는가?"
🚩 한 줄 요약
"때로는 '없는 데이터'가 '있는 데이터'보다 더 많은 것을 말해준다."
매출을 올리는 것만큼 중요한 것이 '왜 그냥 나갔을까?'를 분석하는 것이다.
LEFT JOIN과 IS NULL 조합은 이 질문에 답하기 위한 데이터 분석가의 가장 강력한 무기다.
'Data Science > SQL' 카테고리의 다른 글
| [SQL/오답] 비즈니스 로직에 따른 집합 분리: UNION과 WHERE OR의 (LeetCode1789 Easy) (0) | 2026.02.15 |
|---|---|
| [SQL/오답] 문자열 가공; CONCAT과 SUBSTR/UPPER/LOWER을 활용한 이름 표준화 (LeetCode1667 Easy) (0) | 2026.02.15 |
| [SQL/오답] LIKE의 함정과 REGEXP 활용한 패턴 매칭 (LeetCode 1527 Easy) (0) | 2026.02.15 |
| [SQL/오답] 다차원 그룹화와 중복 제거를 활용한 데모그래픽 지표 산출 (프로그래머스 Lv4) (0) | 2026.02.15 |
| [SQL/오답] 코호트 분석: 고정 분모와 동적 분자를 활용한 구매 비율 산출 (프로그래머스 Lv5) (0) | 2026.02.15 |