본문 바로가기

Dev Log

「기획자의 SQL」을 읽으며 정리한 개념 노트와 생각들

728x90

주말에 『기획자의 SQL: 데이터에는 내러티브가 있다』를 읽게 되었다.

책을 읽으면서, 그동안 개념적으로 정확히 이해하지 못했던 SQL 관련 내용들과

개발자로서 평소 SQL을 어떤 관점에서 작성해왔는지 돌아보게 되었다.

이 글은 책을 읽으며 헷갈렸던 개념들을 정리하고,

“개발자 관점의 SQL”과 “데이터/기획 관점의 SQL”의 차이에 대해 느낀 점을 기록한 글이다.

 

개발할 때 내가 SQL을 바라보던 관점

지금까지 개발하면서 SQL을 작성할 때 주로 고려했던 부분은 다음과 같았다.

  • 올바르게 데이터를 조회하는지
  • 엣지 케이스나 의도하지 않은 버그는 없는지
  • 너무 복잡해서 유지보수가 어려워지지는 않는지
  • 해당 SQL 문의 성능은 괜찮은지

즉, 기능 구현과 안정성 중심의 관점이었다.

반면, *기획자나 데이터 분석가의 관점에서 “이 데이터가 어떤 의미를 가지는가”*를 중심으로

SQL을 고민해본 경험은 거의 없었다.

특히 데이터 분석가와 협업하며 지표를 정의하고 SQL을 함께 설계해본 경험이 없다 보니,

그런 시각 자체가 익숙하지 않았다는 점을 책을 읽으며 깨닫게 되었다.

 

1. 개념적으로 헷갈렸던 것들

1️⃣ DBeaver와 MariaDB의 역할

처음 DBeaver를 설치한 뒤 MariaDB까지 설치하라는 안내를 봤을 때,

**“왜 둘 다 설치해야 하지?”**라는 의문이 들었다.

정리해보면 역할은 명확히 나뉜다.

  • MariaDB
    • 실제로 데이터를 저장하는 서버
    • SQL을 해석하고 실행하는 DB 엔진
    • 집계, JOIN, 정렬 등 모든 연산을 수행
  • DBeaver
    • MariaDB(PostgreSQL 등)에 접속하는 클라이언트 도구
    • 사람이 SQL을 작성하고 결과를 보기 쉽게 보여주는 역할

즉,

👉 SQL을 실제로 실행하는 주체는 DB(MariaDB, PostgreSQL)

👉 DBeaver는 단지 그 DB에 접속하는 도구

 

2️⃣ COUNT 함수는 NULL을 포함하는가?

이 부분도 명확히 정리되지 않은 상태로 사용하고 있었다.

  • COUNT(*)
    • NULL 여부와 상관없이 조건에 맞는 모든 행(row)을 카운트
  • COUNT(컬럼명)
    • 해당 컬럼 값이 NULL이 아닌 경우만 카운트
COUNT(*)-- 행의 개수
COUNT(user_id)-- user_id가 NULL이 아닌 행의 개수

지표를 정의할 때 이 차이를 인지하지 못하면

의도와 다른 숫자를 보고도 눈치채지 못하는 상황이 생길 수 있다.

 

3️⃣ ORDER BY country, uniqueUserCnt DESC 의 의미

ORDERBY country, uniqueUserCntDESC

이 구문을 처음 보면 두 컬럼 모두 DESC로 정렬되는 것처럼 보일 수 있다.

하지만 실제 의미는 다음과 같다.

  • country → 기본값 ASC
  • uniqueUserCnt → DESC

즉,

ORDERBY countryASC, uniqueUserCntDESC

📌 정렬 방향을 명시하지 않으면 기본값은 ASC

 

4️⃣ HAVING과 집계 함수의 개념

HAVING은 WHERE와 자주 헷갈리는 개념 중 하나였다.

이를 이해하기 위해 SQL의 실행 흐름을 다시 정리해보았다.

SQL 실행 흐름 (개념적)

1️⃣ FROM

  • 어떤 테이블(데이터 집합)을 조회할지 결정

2️⃣ WHERE

  • 개별 행(row) 기준 필터링
  • 👉 아직 집계 전 단계

3️⃣ GROUP BY

  • 행들을 의미 있는 단위로 그룹화
  • (국가별, 날짜별, 사용자별 등)

4️⃣ 집계 함수 계산

  • COUNT, SUM, AVG 등의 결과가 이 시점에서 생성됨

5️⃣ HAVING

  • 집계가 끝난 그룹 단위 결과를 조건으로 필터링
  • 👉 집계 결과를 조건으로 사용할 수 있는 유일한 단계

6️⃣ SELECT

  • 최종적으로 출력할 컬럼 결정

7️⃣ ORDER BY

  • 결과 정렬

👉 WHERE는 행을 거르고, HAVING은 그룹을 거른다

 

2. 책을 읽으며 얻은 인사이트

“Getting numbers is easy; Getting numbers you can trust is hard”

숫자를 뽑는 것 자체는 쉽지만,

신뢰할 수 있는 숫자를 만드는 것은 훨씬 어렵다는 문장이 인상 깊었다.

 

1️⃣ 지표 정의의 중요성

데이터를 비즈니스 목적으로 활용하려면

명확하고 일관된 지표 기준이 반드시 필요하다.

예를 들어:

  • 회원 수를 계산할 때
    • COUNT(*) 인가?
    • COUNT(DISTINCT id) 인가?
    • 연락처가 NULL인 회원은 포함할 것인가?

이 기준이 명확하지 않으면,

같은 질문에도 매번 다른 숫자가 나오게 된다.

 

2️⃣ 데이터 발생 기준에 대한 이해

  • “일별 매출”이라는 지표 하나만 보더라도
    • 결제 요청 시점인가?
    • 결제 완료 시점인가?
    • 구매 확정 버튼을 누른 시점인가?

👉 데이터가 언제, 어떤 이벤트에서 발생했는지를 정확히 이해하지 못하면

지표 해석은 쉽게 왜곡된다.

 

3. 데이터 분석 관점에서 자주 사용하는 SQL 패턴들

1️⃣ 데이터 그룹화 (GROUP BY + 집계 함수)

📌 기본 구조

SELECT [컬럼명], [집계 함수]
FROM [테이블명]
WHERE [조건]
GROUPBY [컬럼명]
ORDERBY [컬럼명]ASC|DESC;

  • GROUP BY에 명시된 컬럼 단위로 데이터가 묶임
  • ORDER BY는 그룹 결과를 정렬하는 단계
  • 정렬 방향 생략 시 기본값은 ASC

📌 예시: 연도별 가입 회원 수 추출

SELECT SUBSTR(created_at,1,4)AS years,
COUNT(DISTINCT id)AS userCnt
FROM users
WHERE created_atISNOT NULL
GROUPBY years
ORDERBY yearsDESC;

 

2️⃣ 데이터 결과 집합 결합 (JOIN + SUBQUERY)

📌 기본 구조

SELECT [컬럼명]
FROM
  ( [서브쿼리1] ) [별칭1]
[JOIN TYPE]
  ( [서브쿼리2] ) [별칭2]
ON [별칭1].[KEY]= [별칭2].[KEY];

  • 서브쿼리로 의미 있는 데이터 집합을 먼저 정의
  • 이후 JOIN을 통해 조건에 맞는 데이터만 결합

📌 예시

SELECT*
FROM
  (SELECT id, city, country, is_marketing_agree
FROM users
WHERE is_marketing_agree=1) u
INNERJOIN
  (SELECT user_id, last_name, first_name, birth_date
FROM staff
WHERE birth_date>='1980-01-01') s
ON u.id= s.user_id;

 

3️⃣ 테이블 결합 후 그룹화 (JOIN + GROUP BY)

📌 기본 구조

SELECT [컬럼명], [집계 함수]
FROM [테이블1] [별칭1]
[JOIN TYPE]
[테이블2] [별칭2]
ON [별칭1].[KEY]= [별칭2].[KEY]
GROUPBY [컬럼명]
ORDERBY [집계 결과]ASC|DESC;

  • JOIN 후의 결과 집합을 다시 그룹화
  • JOIN → GROUP BY 순서가 핵심

📌 예시: 제품별 주문 건수 집계

SELECT p.id, p.name,COUNT(DISTINCT od.order_id)AS ordCnt
FROM orderdetails od
LEFTJOIN products p
ON od.product_id= p.id
GROUPBY p.id, p.name
ORDERBY ordCntDESC;

 

4️⃣ 서브쿼리로 필터링하기 (WHERE + SUBQUERY)

📌 기본 구조

SELECT [컬럼명]
FROM
  ( [서브쿼리] ) [별칭]
WHERE [조건]
ORDERBY [컬럼명]ASC|DESC;

  • 집계 결과를 WHERE에서 사용하고 싶을 때 자주 사용
  • HAVING 대신 서브쿼리로 한 번 감싸는 패턴

📌 예시: 국가별 회원 수가 5명 이상인 국가

SELECT a.country, a.userCnt
FROM
  (SELECT country,COUNT(DISTINCT id)AS userCnt
FROM users
GROUPBY country) a
WHERE a.userCnt>=5
ORDERBY a.userCntDESC;

 

5️⃣ 리텐션 분석 (LEFT JOIN)

📌 기본 구조

SELECT
COUNT(DISTINCT [별칭2].[컬럼])/
COUNT(DISTINCT [별칭1].[컬럼])AS [지표명]
FROM
  ( [기준 집합] ) [별칭1]
LEFTJOIN
  ( [비교 집합] ) [별칭2]
ON [별칭1].[KEY]= [별칭2].[KEY];

  • LEFT JOIN을 사용해 기준 집합을 유지
  • 비율 계산 시 분모/분자 의미가 명확해야 함

📌 예시: 리텐션 비율 계산

SELECT
  ROUND(
COUNT(DISTINCT re.user_id)/
COUNT(DISTINCT fst.user_id),
2)AS retentionRatio
FROM
  (SELECT user_id
FROM orders
WHERE order_dateBETWEEN'2015-12-01'AND'2015-12-31') fst
LEFTJOIN
  (SELECT user_id
FROM orders
WHERE order_dateBETWEEN'2016-01-01'AND'2016-01-31') re
ON fst.user_id= re.user_id;

마무리하며

이 책을 통해 느낀 가장 큰 차이는 SQL을 “데이터를 꺼내는 도구”가 아니라 “의미와 기준을 정의하는 언어”로 바라보는 관점이었다.

개발자로서 기능 구현을 위한 SQL을 넘어, 앞으로는 “이 숫자가 어떤 이야기를 하고 있는가”를 조금 더 의식하며 쿼리를 작성해봐야 겠다.

728x90