SQL 백분위와 전년 동기 비교: PERCENT_RANK·LAG·누락 기간 처리
1장. 전년 대비 80% 성장이라고 했는데 실제로는 20%였다#
개발부의 분기별 매출이 다음과 같다고 하겠습니다.
2025 Q1
100
2025 Q3
150
2025 Q4
200
2026 Q1
120
2026 Q3
1802025년 2분기와 2026년 2분기는 데이터가 없습니다.
분석 담당자가 다음과 같이 생각했습니다.
분기 데이터니까
4행 전이 전년도 같은 분기다.그리고 LAG(total_sales, 4)를 사용했습니다.
2026년 3분기 180의 네 행 앞에는:
2025 Q1
100이 있습니다.
그래서 계산된 증가율은:
(180 - 100) / 100 × 100
=
80%입니다.
하지만 2026년 3분기의 실제 전년도 같은 분기는:
2025 Q3
150입니다.
정확한 전년 동기 증가율은:
(180 - 150) / 150 × 100
=
20%입니다.
SQL 문법에는 오류가 없습니다.
문제는:
행의 위치를 달력상의 기간 관계라고 착각한 것
입니다.
2장. 분석 SQL에서는 “이전 행”과 “이전 기간”을 구분해야 한다#
다음 두 질문은 서로 다릅니다.
이 행 바로 앞에는 어떤 값이 있는가?그리고:
이 기간의 전년도 같은 기간은 무엇인가?첫 번째는 정렬된 행의 위치 문제입니다.
두 번째는 시간상의 관계 문제입니다.
LAG는 첫 번째 문제를 해결합니다.
전년 동기 비교는 두 번째 문제입니다.
따라서:
LAG
=
전년 동기 함수라고 이해하면 안 됩니다.
3장. 이번 글에서는 두 종류의 상대 비교를 함께 다룬다#
첫 번째는 직원 급여의 상대 위치입니다.
이 직원의 급여는
비교 집단에서 어느 정도 위치인가?이 질문에는:
PERCENT_RANK를 사용할 수 있습니다.
두 번째는 기간 매출 비교입니다.
이번 분기의 매출은
작년 같은 분기보다 얼마나 달라졌는가?이 질문에는:
전년도 기간 연결이 필요합니다.
두 계산 모두 공통점이 있습니다.
무엇을 비교 집단으로 볼 것인지 먼저 정의해야 한다.
4장. 먼저 급여 실습 데이터를 준비하자#
부서 테이블을 만듭니다.
CREATE TABLE department (
dept_id integer PRIMARY KEY,
dept_name text NOT NULL
);직원 테이블을 만듭니다.
CREATE TABLE employee (
emp_id integer PRIMARY KEY,
emp_name text NOT NULL,
dept_id integer REFERENCES department(dept_id),
salary integer NOT NULL,
mgr_id integer REFERENCES employee(emp_id),
retire_date date
);부서를 입력합니다.
INSERT INTO department
VALUES
(10, '개발'),
(20, '영업'),
(30, '인사');직원 데이터를 넣습니다.
INSERT INTO employee
VALUES
(1, '가람', 10, 600, NULL, NULL),
(2, '나래', 10, 500, 1, NULL),
(3, '다온', 10, 500, 1, NULL),
(4, '라온', 20, 300, 1, NULL),
(5, '마루', 20, 100, 4, NULL),
(6, '바다', NULL, 400, 1, NULL),
(7, '사라', 20, 900, 4, DATE '2026-01-01');5장. 비교 집단을 먼저 고정해야 한다#
이번 급여 분석에서는 퇴직자를 제외합니다.
따라서 비교 집단은:
가람 600
나래 500
다온 500
라온 300
마루 100
바다 400총 6명입니다.
오름차순으로 정렬하면:
100
300
400
500
500
600입니다.
이 여섯 행이 PERCENT_RANK의 비교 집단입니다.
6장. 퇴직자를 포함하면 같은 직원의 백분위도 바뀐다#
퇴직한 사라의 급여는:
900입니다.
사라를 포함하면 비교 행은 7개가 됩니다.
즉 같은 직원의 급여가 변하지 않아도:
비교 인원
순위
분모가 달라질 수 있습니다.
백분위 결과를 해석할 때는 반드시:
어느 집단 안에서 계산했는가?
를 함께 밝혀야 합니다.
7장. PERCENT_RANK의 기본 공식#
PERCENT_RANK는 개념적으로 다음 식을 사용합니다.
(RANK - 1)
/
(전체 행 수 - 1)급여 500인 직원은 오름차순으로 공동 4위입니다.
전체 행 수:
6이므로:
(4 - 1)
/
(6 - 1)
=
3 / 5
=
0.6입니다.
백분율로 표시하면:
60%입니다.
8장. 급여 500 두 명은 같은 PERCENT_RANK를 가진다#
나래:
500다온:
500입니다.
급여 기준으로 동점이므로 둘 다:
RANK = 4입니다.
따라서:
PERCENT_RANK
=
0.6으로 같습니다.
9장. ROW_NUMBER와 PERCENT_RANK는 다른 질문을 푼다#
ROW_NUMBER는 각 행에 고유한 순번을 줍니다.
예를 들어:
ROW_NUMBER() OVER (
ORDER BY salary DESC, emp_id
)를 사용하면 급여가 같은 나래와 다온도:
2번
3번처럼 서로 다른 번호를 받습니다.
반면 PERCENT_RANK를 급여만 기준으로 계산하면 두 직원은 같은 상대 위치를 가집니다.
10장. 같은 데이터에서 두 함수를 함께 계산해 보자#
WITH ranked AS (
SELECT
emp_id,
emp_name,
dept_id,
salary,
ROW_NUMBER() OVER (
PARTITION BY dept_id
ORDER BY salary DESC, emp_id
) AS dept_row,
PERCENT_RANK() OVER (
ORDER BY salary
) AS pct_rank
FROM employee
WHERE retire_date IS NULL
)
SELECT
emp_id,
emp_name,
dept_id,
salary,
dept_row,
ROUND(
(pct_rank * 100)::numeric,
1
) AS pct_rank_100
FROM ranked
ORDER BY
salary,
emp_id;예상되는 핵심 값은 다음과 같습니다.
| 이름 | 급여 | 전체 백분위 값 |
|---|---|---|
| 마루 | 100 | 0.0 |
| 라온 | 300 | 20.0 |
| 바다 | 400 | 40.0 |
| 나래 | 500 | 60.0 |
| 다온 | 500 | 60.0 |
| 가람 | 600 | 100.0 |
11장. PERCENT_RANK 60을 “60%의 직원보다 급여가 높다”라고 단정하지 말자#
PERCENT_RANK는:
공동 순위
전체 행 수를 이용한 상대 위치 지표입니다.
급여 500의 값이:
0.6이라고 해서 단순히:
정확히 전체 직원의 60%보다 급여가 높다.
라고 읽으면 동점 때문에 의미가 어긋날 수 있습니다.
실제로 급여 500보다 낮은 직원은:
100
300
400세 명입니다.
전체 여섯 명 중:
3 / 6
=
50%입니다.
따라서 PERCENT_RANK는 “나보다 낮은 사람의 비율”과 동일한 개념이 아닙니다.
12장. 백분위 함수는 이름이 비슷해도 의미가 다르다#
윈도우 함수에는 다음과 같은 함수들이 있습니다.
PERCENT_RANK
CUME_DIST
NTILE각각 의미가 다릅니다.
예를 들어 CUME_DIST는 현재 값 이하의 행 비율과 관련된 누적 분포를 표현합니다.
PERCENT_RANK와 같은 숫자가 나온다고 가정하면 안 됩니다.
분석에서는 함수 이름보다 정확한 정의를 확인해야 합니다.
13장. 급여 500인 직원 한 명을 더 추가하면 값이 바뀐다#
새 직원의 급여도:
500이라고 하겠습니다.
전체 행 수는:
7이 됩니다.
500의 공동 순위는 여전히:
4입니다.
하지만 PERCENT_RANK는:
(4 - 1)
/
(7 - 1)
=
3 / 6
=
0.5가 됩니다.
즉 기존 직원의 급여가 변하지 않았는데:
60%
→
50%로 바뀝니다.
14장. 백분위는 값 자체보다 비교 집단에 의존한다#
따라서 백분위 보고서에는 최소한 다음이 필요합니다.
비교 대상
필터 조건
기준일
정렬 기준예:
2026-10-01 재직자 기준
퇴직자 제외
전사 급여 기준같은 설명이 있어야 동일한 값을 다시 계산할 수 있습니다.
15장. 부서 미배정 직원도 하나의 파티션을 만들 수 있다#
바다의 dept_id는:
NULL입니다.
다음 함수:
ROW_NUMBER() OVER (
PARTITION BY dept_id
ORDER BY salary DESC
)에서는 dept_id IS NULL인 행도 하나의 파티션으로 처리됩니다.
바다 혼자라면 해당 파티션에서:
dept_row = 1이 됩니다.
16장. 미배정 직원을 제거하면 전체 백분위 분모도 달라진다#
업무에서:
부서가 없는 직원은 분석 제외라고 정했다고 하겠습니다.
다음 조건을 추가합니다.
WHERE retire_date IS NULL
AND dept_id IS NOT NULL그러면 비교 집단은:
6명
→
5명으로 줄어듭니다.
결과적으로 모든 PERCENT_RANK의 분모가 달라질 수 있습니다.
단순히 바다 행 하나만 결과에서 사라지는 문제가 아닙니다.
17장. 계산 후 필터링과 계산 전 필터링은 다르다#
첫 번째 방식:
활성 직원 전체에서 백분위 계산
↓
그 뒤 일부 직원만 표시두 번째 방식:
일부 직원만 먼저 남김
↓
그 집단에서 백분위 계산은 서로 다른 결과를 만듭니다.
윈도우 함수에서는 특히:
보여줄 대상과 계산할 집단을 구분해야 한다.
는 원칙이 중요합니다.
18장. 부서별 상위 3명만 보여줘도 백분위는 전체 기준으로 유지할 수 있다#
예를 들어:
WITH ranked AS (
SELECT
emp_id,
emp_name,
dept_id,
salary,
ROW_NUMBER() OVER (
PARTITION BY dept_id
ORDER BY salary DESC, emp_id
) AS dept_row,
PERCENT_RANK() OVER (
ORDER BY salary
) AS pct_rank
FROM employee
WHERE retire_date IS NULL
)
SELECT *
FROM ranked
WHERE dept_row <= 3;처럼 하면 먼저 전체 활성 직원 기준으로 백분위를 계산합니다.
그 뒤 부서 상위 3명만 출력합니다.
19장. 반대로 필터 후 계산하면 완전히 다른 질문이 된다#
먼저 상위 직원만 남긴 뒤 다시 PERCENT_RANK를 계산하면 질문은:
전체 재직자 중 위치가 아니라:
선택된 상위 직원들 안에서의 위치로 바뀝니다.
숫자는 비슷해 보여도 의미가 다릅니다.
20장. 부서 내부 백분위를 원한다면 PARTITION BY를 사용한다#
전사 기준이 아니라:
각 부서 안에서의 급여 상대 위치
를 보고 싶다고 하겠습니다.
PERCENT_RANK() OVER (
PARTITION BY dept_id
ORDER BY salary
)을 사용할 수 있습니다.
개발부와 영업부의 순위 계산이 각각 별도로 시작됩니다.
21장. 전사 60%와 부서 50%는 동시에 존재할 수 있다#
어떤 직원의 급여가:
전사 기준
PERCENT_RANK = 0.6이고:
부서 기준
PERCENT_RANK = 0.5일 수 있습니다.
둘 중 하나가 틀린 것이 아닙니다.
비교 집단이 다릅니다.
따라서 지표명도:
salary_percent_rank하나보다:
company_salary_percent_rank
department_salary_percent_rank처럼 명확하게 만드는 편이 좋습니다.
22장. 최고 급여라고 PERCENT_RANK가 항상 1인 것은 아니다#
급여 데이터:
100
200
300
300이라고 하겠습니다.
최고 급여 300이 두 명입니다.
공동 순위는:
3입니다.
전체 행 수는:
4입니다.
따라서:
(3 - 1)
/
(4 - 1)
=
2 / 3
≈
0.667입니다.
최고값인데도 1이 아닙니다.
동점이 있기 때문입니다.
23장. 최고값을 반드시 100%로 표현해야 한다면 지표 정의를 다시 봐야 한다#
업무 담당자가:
최고 급여는 반드시 100점이어야 합니다.
라고 한다면 PERCENT_RANK의 정의가 요구에 맞지 않을 수 있습니다.
이 경우 필요한 것이:
PERCENT_RANK인지:
CUME_DIST
등급 점수
직접 정의한 정규화 점수인지 다시 확인해야 합니다.
함수의 결과를 업무 요구에 억지로 맞추기보다 적절한 지표를 선택해야 합니다.
24장. 비교 집단이 한 명이면 특별한 경계 조건이 생긴다#
직원이 한 명뿐이라고 하겠습니다.
개념 공식:
(RANK - 1)
/
(N - 1)에서는:
0 / 0문제가 생깁니다.
PostgreSQL의 PERCENT_RANK는 이런 단일 행 파티션에서 0을 반환합니다.
직접 공식을 구현한다면 0 나눗셈 처리가 필요합니다.
25장. 함수의 경계 동작을 직접 공식과 같다고 가정하지 말자#
DBMS의 내장 함수는 경계 조건을 정의해 놓습니다.
따라서:
직접 구현한 수식
DBMS 함수가 항상 완전히 같은 동작을 한다고 가정하면 안 됩니다.
특히:
행 1개
NULL
동점
빈 집합같은 경계에서는 제품 문서의 정의를 확인하는 것이 좋습니다.
26장. 이제 분기별 매출 실습 데이터를 만들자#
CREATE TABLE quarterly_sales (
dept_id integer NOT NULL,
sales_year integer NOT NULL,
quarter_no integer NOT NULL
CHECK (quarter_no BETWEEN 1 AND 4),
total_sales integer NOT NULL,
PRIMARY KEY (
dept_id,
sales_year,
quarter_no
)
);데이터:
INSERT INTO quarterly_sales
VALUES
(10, 2025, 1, 100),
(10, 2025, 3, 150),
(10, 2025, 4, 200),
(10, 2026, 1, 120),
(10, 2026, 3, 180);27장. 일부러 분기를 비워 둔 이유#
없는 기간:
2025 Q2
2026 Q2입니다.
이 누락 때문에:
네 행 전
=
전년 동기라는 가정이 깨집니다.
실무에서도 이런 상황은 흔합니다.
시스템 오픈 이전 기간
데이터 수집 누락
부서 신설
영업 중단
집계 실패등 때문에 기간 행이 빠질 수 있습니다.
28장. 먼저 잘못된 LAG 4 방식을 재현해 보자#
SELECT
dept_id,
sales_year,
quarter_no,
total_sales,
LAG(total_sales, 4) OVER (
PARTITION BY dept_id
ORDER BY sales_year, quarter_no
) AS lag_4_sales
FROM quarterly_sales
ORDER BY
dept_id,
sales_year,
quarter_no;정렬된 행:
1. 2025 Q1 100
2. 2025 Q3 150
3. 2025 Q4 200
4. 2026 Q1 120
5. 2026 Q3 180입니다.
29장. 2026 Q3의 네 행 전은 2025 Q1이다#
다섯 번째 행:
2026 Q3
180에서 네 행 전:
2025 Q1
100입니다.
따라서 LAG(4) 결과는:
100입니다.
하지만 실제 전년도 같은 분기는:
2025 Q3
150입니다.
30장. 행 위치는 기간 의미를 모른다#
LAG는 다음을 알고 있습니다.
정렬 순서
몇 행 전하지만 다음은 모릅니다.
분기
회계연도
윤년
휴일
영업일
전년 동기따라서 시간 의미는 사용자가 SQL 구조로 표현해야 합니다.
31장. 전년 동기 비교는 기간 키로 직접 연결하는 것이 명확하다#
현재 분기 c와 전년 분기 p를 조인합니다.
조건:
같은 부서
전년도
같은 분기입니다.
SQL:
SELECT
c.dept_id,
c.sales_year,
c.quarter_no,
c.total_sales,
p.total_sales AS prior_year_sales
FROM quarterly_sales AS c
LEFT JOIN quarterly_sales AS p
ON p.dept_id = c.dept_id
AND p.sales_year = c.sales_year - 1
AND p.quarter_no = c.quarter_no
ORDER BY
c.dept_id,
c.sales_year,
c.quarter_no;32장. 2026 Q3은 정확하게 2025 Q3과 연결된다#
현재:
2026 Q3
180조인 조건:
sales_year = 2025
quarter_no = 3따라서:
prior_year_sales = 150입니다.
행 순서가 어떻게 되어 있든 상관없습니다.
33장. 전년 동기 증가율을 계산하자#
일반적인 증가율:
(현재 - 전년)
/
전년
× 100입니다.
2026 Q3:
(180 - 150)
/
150
× 100
=
20%입니다.
34장. SQL로 계산하면#
SELECT
c.dept_id,
c.sales_year,
c.quarter_no,
c.total_sales,
p.total_sales AS prior_year_sales,
ROUND(
100.0
* (c.total_sales - p.total_sales)
/ NULLIF(p.total_sales, 0),
1
) AS yoy_pct
FROM quarterly_sales AS c
LEFT JOIN quarterly_sales AS p
ON p.dept_id = c.dept_id
AND p.sales_year = c.sales_year - 1
AND p.quarter_no = c.quarter_no
ORDER BY
c.dept_id,
c.sales_year,
c.quarter_no;35장. 결과를 확인하면#
| 기간 | 현재 매출 | 전년도 같은 분기 | 전년비 |
|---|---|---|---|
| 2025 Q1 | 100 | NULL | NULL |
| 2025 Q3 | 150 | NULL | NULL |
| 2025 Q4 | 200 | NULL | NULL |
| 2026 Q1 | 120 | 100 | 20.0% |
| 2026 Q3 | 180 | 150 | 20.0% |
2025년 행에는 2024년 데이터가 없습니다.
따라서 전년비도 없습니다.
36장. 비교 행이 없다는 것과 전년 매출이 0이라는 것은 다르다#
두 상황을 비교해 보겠습니다.
경우 A#
2025 Q2 행 자체가 없음경우 B#
2025 Q2 행 존재
매출 = 0두 상황은 전혀 다릅니다.
A는:
비교 자료 없음일 수 있습니다.
B는:
실제 매출 0입니다.
37장. 누락 기간을 0으로 채우면 의미가 바뀔 수 있다#
원본에:
2025 Q2
없음인데 보고 편의를 위해:
2025 Q2
0으로 만든다고 하겠습니다.
이것은:
그 기간 매출이 실제로 0원이었다.
라는 새로운 사실을 만들어 버릴 수 있습니다.
따라서:
미수집
영업 없음
실제 매출 0을 구분해야 합니다.
38장. 전년 매출이 0이면 일반적인 증가율은 계산할 수 없다#
전년:
0올해:
120이라고 하겠습니다.
증가율 공식:
(120 - 0)
/
0은 정의할 수 없습니다.
이 값을:
12000%
무한대%
100%등으로 임의 처리하면 안 됩니다.
업무 보고 규칙을 별도로 정해야 합니다.
39장. NULLIF로 0 나눗셈을 막을 수 있다#
NULLIF(p.total_sales, 0)은 전년 매출이 0일 때:
NULL을 반환합니다.
따라서 나눗셈 오류를 피할 수 있습니다.
하지만 NULLIF가:
전년 매출 0
→ 증가율 0%로 바꿔주는 것은 아닙니다.
결과는 계산 불가 상태에 가깝습니다.
40장. 네 가지 전년 비교 상황을 구분하자#
| 전년 | 올해 | 증감액 | 일반적인 증감률 해석 |
|---|---|---|---|
| 100 | 120 | +20 | 20% |
| 100 | 0 | -100 | -100% |
| 0 | 120 | +120 | 분모 0으로 정의 불가 |
| 행 없음 | 120 | 알 수 없음 | 비교 자료 없음 |
특히 마지막 두 행을 같은 NULL 하나로만 보여주면 원인이 사라질 수 있습니다.
41장. 계산 상태를 별도 컬럼으로 만들 수 있다#
예:
CASE
WHEN p.dept_id IS NULL
THEN 'NO_PRIOR_PERIOD'
WHEN p.total_sales = 0
THEN 'ZERO_DENOMINATOR'
ELSE 'OK'
END AS yoy_status이렇게 하면:
비교 자료 없음
분모 0
정상 계산을 구분할 수 있습니다.
42장. 완성형 전년 동기 조회 예제#
SELECT
c.dept_id,
c.sales_year,
c.quarter_no,
c.total_sales AS current_sales,
p.total_sales AS prior_year_sales,
CASE
WHEN p.dept_id IS NULL
THEN 'NO_PRIOR_PERIOD'
WHEN p.total_sales = 0
THEN 'ZERO_DENOMINATOR'
ELSE 'OK'
END AS yoy_status,
CASE
WHEN p.dept_id IS NULL
THEN NULL
WHEN p.total_sales = 0
THEN NULL
ELSE ROUND(
100.0
* (c.total_sales - p.total_sales)
/ p.total_sales,
1
)
END AS yoy_pct
FROM quarterly_sales AS c
LEFT JOIN quarterly_sales AS p
ON p.dept_id = c.dept_id
AND p.sales_year = c.sales_year - 1
AND p.quarter_no = c.quarter_no
ORDER BY
c.dept_id,
c.sales_year,
c.quarter_no;43장. 기간당 여러 거래가 있다면 먼저 집계해야 한다#
지금 quarterly_sales는 한 행이:
부서 + 연도 + 분기입니다.
하지만 원천 거래 테이블은 한 분기에 수천 건일 수 있습니다.
그 상태에서 바로 자기 자신과 전년 동기 조인을 하면 여러 행이 서로 곱해질 수 있습니다.
따라서 먼저:
분기 단위 집계를 만들고 비교해야 합니다.
44장. 거래 원천에서 분기 집계를 만드는 예#
WITH quarterly AS (
SELECT
dept_id,
EXTRACT(YEAR FROM sold_at)::integer
AS sales_year,
EXTRACT(QUARTER FROM sold_at)::integer
AS quarter_no,
SUM(amount) AS total_sales
FROM sales
GROUP BY
dept_id,
EXTRACT(YEAR FROM sold_at),
EXTRACT(QUARTER FROM sold_at)
)
SELECT *
FROM quarterly;이 결과에서:
한 행
=
한 부서의 한 분기가 되어야 합니다.
45장. 그레인을 확인하지 않고 전년 동기 조인하면 매출이 중복될 수 있다#
2025 Q1 거래가:
3건이고 2026 Q1 거래가:
4건이라고 하겠습니다.
거래 행 그대로:
2026 Q1
JOIN
2025 Q1하면 최대:
4 × 3
=
12행이 만들어질 수 있습니다.
따라서 기간 비교 전에 집계 단위를 고정해야 합니다.
46장. LAG가 항상 나쁜 것은 아니다#
기간 데이터가 실제로 다음 조건을 만족한다고 하겠습니다.
부서별
분기당 정확히 한 행
누락 분기 없음
연속된 기간그렇다면:
LAG(total_sales, 4)로 전년 같은 분기를 가져올 수 있습니다.
문제는 함수 자체가 아니라 전제 조건을 확인하지 않은 것입니다.
47장. 연속된 캘린더 그리드를 먼저 만들 수도 있다#
분기 행이 빠지는 것이 문제라면:
2025 Q1
2025 Q2
2025 Q3
2025 Q4
2026 Q1
2026 Q2
2026 Q3
2026 Q4처럼 모든 기간을 가진 캘린더 테이블을 만들 수 있습니다.
원본 매출을 여기에 LEFT JOIN합니다.
그러면 시간 축 자체는 연속됩니다.
48장. 하지만 빈 분기의 값을 무엇으로 채울지는 여전히 업무 문제다#
캘린더를 만들었다고 해서:
빈 매출
→ 0으로 자동 결정되는 것은 아닙니다.
가능한 의미:
실제 매출 0
데이터 미수집
부서 미존재
영업 중단등이 있습니다.
캘린더 테이블은 기간을 만들어 줄 뿐 데이터 의미까지 결정하지 않습니다.
49장. LAG 1과 LAG 4도 의미가 다르다#
분기 자료에서:
LAG(total_sales, 1)은:
직전 행을 가져옵니다.
연속 분기라면 직전 분기일 수 있습니다.
LAG(total_sales, 4)는:
네 행 전입니다.
연속 분기라는 전제가 있다면 전년 동기가 될 수 있습니다.
항상 달력 관계를 직접 보장하는 것은 아닙니다.
50장. 월별 데이터에서도 같은 문제가 생긴다#
월별 매출에서:
LAG(total_sales, 12)을 사용하는 경우가 있습니다.
모든 월이 정확히 한 행씩 존재한다면 전년도 같은 월을 의미할 수 있습니다.
하지만 몇 달이 누락되어 있으면:
12행 전
≠
12개월 전이 됩니다.
원리는 분기와 같습니다.
51장. 일별 데이터에서는 휴일 때문에 더 자주 문제가 생긴다#
영업일 데이터만 저장한다고 하겠습니다.
금요일의:
LAG(value, 1)은 목요일일 수 있습니다.
월요일의:
LAG(value, 1)은 지난 금요일입니다.
즉:
이전 행과:
하루 전은 다른 개념입니다.
52장. 주식·금융 데이터에서는 이전 거래일이라는 개념이 따로 필요할 수 있다#
주말과 휴일에는 거래가 없습니다.
이 경우:
전일이라는 표현이:
달력상 하루 전인지:
직전 거래일인지 먼저 정해야 합니다.
LAG는 정렬된 데이터의 이전 행을 가져오기 때문에 직전 거래일 계산에는 잘 맞을 수 있지만 달력 하루 전과는 다릅니다.
53장. 전년 동기 비교에서 회계연도도 확인해야 한다#
모든 조직이:
1월~12월을 동일한 회계연도로 사용하는 것은 아닙니다.
어떤 조직은:
4월~다음 해 3월을 회계연도로 볼 수 있습니다.
이 경우 단순한:
sales_year - 1이 업무상 전년도와 다를 수 있습니다.
회계 달력 테이블을 사용하는 이유입니다.
54장. 53주 회계연도에서는 전년도 주차가 단순하지 않을 수 있다#
주 단위 분석에서도:
2025년 52주
2026년 53주처럼 기간 수가 달라질 수 있습니다.
이때:
52행 전이 전년도 같은 영업주라는 보장은 없습니다.
시간 분석에서는 “몇 행 전”보다 기간 식별자의 대응 관계가 더 안전한 경우가 많습니다.
55장. 날짜 조인을 사용할 때도 키의 의미를 명확하게 해야 한다#
예:
sales_year
quarter_no가 실제 달력 기준인지:
fiscal_year
fiscal_quarter인지 구분해야 합니다.
컬럼 이름만 보고 의미를 추정하면 안 됩니다.
56장. 전년비와 증감액은 함께 보여주는 것이 좋다#
2025 Q3:
1502026 Q3:
180증감액:
+30증감률:
+20%입니다.
둘을 함께 보여주면 해석하기 쉽습니다.
작은 기준값에서 매우 높은 비율이 나오는 문제도 파악할 수 있습니다.
57장. 전년 매출 1에서 올해 10이면 900% 증가다#
계산:
(10 - 1)
/
1
× 100
=
900%입니다.
숫자는 맞습니다.
하지만 증가액은:
9뿐입니다.
따라서 비율만 보고 사업 영향이 매우 크다고 판단하면 왜곡될 수 있습니다.
58장. 반대로 큰 금액은 낮은 증가율이어도 영향이 클 수 있다#
전년:
1억올해:
1억 500만증가율:
5%입니다.
증가액은:
500만입니다.
따라서 보고서에는:
현재값
전년값
증감액
증감률을 함께 보여주는 편이 좋습니다.
59장. PERCENT_RANK와 매출 증가율은 모두 %처럼 보여도 전혀 다른 값이다#
급여:
PERCENT_RANK
60%매출:
YoY
20%둘 다 퍼센트처럼 표시됩니다.
하지만 의미는 다릅니다.
PERCENT_RANK#
비교 집단 내 상대 순위YoY 증가율#
두 시점 값의 변화율같은 % 기호 때문에 서로 비슷한 지표라고 생각하면 안 됩니다.
60장. 숫자의 단위도 메타데이터다#
예:
pct_rank = 0.6인지:
pct_rank_percent = 60인지 컬럼만 보고 모호할 수 있습니다.
따라서:
salary_percent_rank와 같은 이름이나 데이터 정의에서 값 범위를 명확히 하는 것이 좋습니다.
61장. 증가율도 0.2와 20을 혼동하기 쉽다#
어떤 시스템은:
0.2를 저장하고 화면에서:
20%으로 표시합니다.
다른 시스템은 DB에:
20을 저장합니다.
통합 시 이를 모르고 합치면 100배 차이가 날 수 있습니다.
지표는 값뿐 아니라 단위 규칙까지 정의해야 합니다.
62장. 백분위 결과를 저장할지 매번 계산할지도 결정해야 한다#
직원의 백분위를 테이블에 저장한다고 하겠습니다.
오늘:
가람
100%입니다.
내일 고급여 직원이 입사하면 백분위가 바뀔 수 있습니다.
원래 직원의 급여가 그대로여도 결과가 변합니다.
따라서 백분위는 비교 집단에 종속된 파생값입니다.
63장. 저장한다면 기준일과 비교 집단도 함께 저장해야 한다#
예:
employee_id
1
as_of_date
2026-10-01
population
ACTIVE_EMPLOYEES
percent_rank
1.0처럼 기준을 남길 수 있습니다.
그렇지 않으면 나중에:
왜 당시에는 80%였는데 지금은 70%인가?
를 설명하기 어렵습니다.
64장. 기간 분석도 기준일을 함께 기록해야 한다#
2026 Q3 보고서를 10월 1일에 만들었습니다.
10월 10일에 늦게 들어온 거래가 반영되었습니다.
같은 2026 Q3 매출이 달라질 수 있습니다.
따라서:
period
2026 Q3
report_as_of
2026-10-01같은 기준이 필요할 수 있습니다.
65장. 매출 기간의 값이 나중에 바뀌면 전년비도 바뀐다#
기존:
2026 Q3
180수정 후:
2026 Q3
210전년:
150이라면 전년비는:
20%
→
40%로 바뀝니다.
파생 지표는 원천 값 변경에 따라 다시 계산되어야 합니다.
66장. 기간당 한 행이라는 제약도 중요하다#
현재 테이블의 기본키:
dept_id
sales_year
quarter_no는 동일 부서·연도·분기에 두 행이 들어오는 것을 막습니다.
이 제약 덕분에:
한 행
=
한 부서의 한 분기라는 가정을 신뢰할 수 있습니다.
67장. 이 제약이 없다면 자기 조인이 여러 행으로 늘어날 수 있다#
2025 Q3에 두 행:
150
202026 Q3에도 두 행:
180
30이 있다면 전년 조인에서:
2 × 2
=
4행이 될 수 있습니다.
따라서 기간 비교 전에 그레인을 보장해야 합니다.
68장. 원천 거래를 집계할 때도 중복 이벤트를 확인해야 한다#
주문 이벤트가 재전송되어 한 거래가 두 번 들어갔다고 하겠습니다.
분기 집계:
180
→
200으로 부풀 수 있습니다.
전년 동기 SQL 자체는 정확해도 입력 집계가 잘못됐습니다.
분석 SQL의 정확성은 원천 데이터 품질과 분리할 수 없습니다.
69장. SQL 분석에서 가장 먼저 해야 할 검산#
2026 Q3:
현재
180
전년
150을 사람이 직접 계산합니다.
증감액
30
증감률
20%입니다.
SQL 결과가:
80%라면 바로 잘못된 비교 기간을 의심할 수 있습니다.
70장. 쿼리 작성 전에 기대 행 수도 적어 보자#
현재 데이터는 총:
5개 분기 행입니다.
자기 조인을 LEFT JOIN했으므로 결과도:
5행이어야 합니다.
만약:
7행
10행이 나온다면 기간 키가 중복되거나 조인 조건이 부족한지 확인해야 합니다.
71장. 백분위에서도 예상 결과를 손으로 계산할 수 있다#
활성 직원 급여:
100
300
400
500
500
600입니다.
예상 PERCENT_RANK:
100
0.0
300
0.2
400
0.4
500
0.6
500
0.6
600
1.0입니다.
이 값과 SQL 결과를 비교합니다.
72장. 동점 처리 검산은 특히 중요하다#
만약 SQL 결과가:
나래 0.6
다온 0.8처럼 나온다면 두 직원의 동일 급여가 서로 다른 순위를 받은 것입니다.
윈도우 ORDER BY 안에:
emp_id같은 추가 열을 넣어 동점을 깨버렸는지 확인해야 합니다.
73장. 표시 순서와 순위 계산 기준을 분리하자#
백분위 계산:
PERCENT_RANK() OVER (
ORDER BY salary
)화면 표시:
ORDER BY salary, emp_id처럼 만들 수 있습니다.
그러면:
급여가 같으면 같은 백분위
화면에서는 안정적인 순서를 동시에 얻을 수 있습니다.
74장. 반대로 ORDER BY에 사번까지 넣으면 순위 의미가 달라진다#
PERCENT_RANK() OVER (
ORDER BY salary, emp_id
)라고 하면 급여가 같아도 사번이 다르기 때문에 서로 다른 정렬 위치를 가질 수 있습니다.
업무 요구가:
급여가 같으면 같은 상대 순위라면 의도와 다릅니다.
75장. 분석 함수에서 ORDER BY는 단순 출력 순서가 아니다#
일반 SELECT의:
ORDER BY는 최종 결과의 표시 순서를 정합니다.
윈도우 함수 내부의:
OVER (
ORDER BY ...
)는 계산 자체의 의미를 결정합니다.
같은 ORDER BY라는 표현이지만 역할이 다릅니다.
76장. 전년 동기에서도 정렬 순서와 비교 관계를 분리해야 한다#
최종 화면은:
최신 분기부터보여줄 수 있습니다.
ORDER BY sales_year DESC, quarter_no DESC하지만 전년 연결 조건은 여전히:
sales_year - 1
같은 quarter_no입니다.
화면 정렬이 바뀌어도 비교 관계는 변하지 않아야 합니다.
77장. 기간 키를 문자열로 만들 때도 주의하자#
예:
2026-Q3처럼 표시할 수 있습니다.
하지만 기간 계산에 문자열 파싱을 반복하기보다:
sales_year
quarter_no처럼 의미를 구조화한 열로 가지고 있는 편이 조인과 제약에 유리합니다.
78장. 분기 번호 유효성도 제약으로 막을 수 있다#
현재:
CHECK (
quarter_no BETWEEN 1 AND 4
)가 있습니다.
따라서:
Q5같은 잘못된 분기가 들어오는 것을 막을 수 있습니다.
분석 단계에서 계속 정제하는 것보다 가능한 오류는 입력 단계에서 차단하는 편이 좋습니다.
79장. 전년도 기간이 여러 개 존재하면 계산을 멈춰야 한다#
정상 구조에서는:
부서 10
2025 Q3가 한 행이어야 합니다.
두 행이 존재한다면:
어느 것을 전년값으로 써야 하는가?라는 문제가 생깁니다.
단순히:
MAX
AVG로 임의 해결하기보다 원천 중복의 의미를 먼저 확인해야 합니다.
80장. 기간 누락을 발견하는 SQL도 만들 수 있다#
2025년에 어떤 분기가 빠졌는지 확인한다고 하겠습니다.
필요한 분기 집합:
1
2
3
4와 실제 데이터를 비교할 수 있습니다.
예를 들어 generate_series를 사용하면:
SELECT q AS missing_quarter
FROM generate_series(1, 4) AS q
WHERE NOT EXISTS (
SELECT 1
FROM quarterly_sales AS s
WHERE s.dept_id = 10
AND s.sales_year = 2025
AND s.quarter_no = q
);결과:
2입니다.
81장. 누락 발견과 누락값 보정은 다른 작업이다#
위 SQL은:
2025 Q2가 없다.는 사실을 알려줍니다.
하지만:
그러므로 매출 0이다.라고 말하지는 않습니다.
누락 탐지와 결측값 해석은 분리해야 합니다.
82장. 같은 원칙은 월·주·일 데이터에도 적용된다#
기간 데이터에서는 항상 세 단계를 구분하면 좋습니다.
1. 어떤 기간이 존재해야 하는가?
2. 실제 어떤 기간이 존재하는가?
3. 없는 기간의 의미는 무엇인가?이 세 질문을 건너뛰고 LAG부터 쓰면 비교 기준이 쉽게 틀어집니다.
83장. 전년비 보고서에 표시하면 좋은 컬럼#
예:
dept_id
current_period
current_sales
prior_period
prior_sales
change_amount
change_pct
comparison_status입니다.
20% 숫자 하나보다 훨씬 설명력이 높습니다.
84장. 백분위 보고서에도 계산 조건을 함께 남길 수 있다#
예:
employee_id
salary
percent_rank
population_count
population_filter
as_of_date입니다.
같은 직원의 값이 나중에 달라져도 비교 집단 변화인지 확인할 수 있습니다.
85장. 분석 SQL의 결과가 바뀌는 세 가지 주요 이유#
첫 번째:
원천 값이 바뀜두 번째:
비교 집단이 바뀜세 번째:
계산 규칙이 바뀜입니다.
이 세 가지를 구분하지 않으면:
왜 지난달 보고서와 숫자가 다릅니까?
라는 질문에 답하기 어렵습니다.
86장. PERCENT_RANK 값이 바뀌었다고 급여가 바뀐 것은 아니다#
직원 급여:
500은 그대로입니다.
하지만 새로운 직원이 추가되면서:
0.6
→
0.5가 됐습니다.
원인은:
비교 집단 변화입니다.
상대 지표의 특징입니다.
87장. 전년비가 바뀌었다고 현재 매출만 바뀐 것도 아니다#
현재 매출:
180은 그대로인데 전년도 자료가:
150
→
160으로 정정됐다고 하겠습니다.
전년비:
20%
→
12.5%로 달라집니다.
상대 비교 지표에서는 기준값 변화도 중요합니다.
88장. 보고서 재현성을 위해 계산 시점의 입력 버전이 필요할 수 있다#
예:
2026 Q3 보고서
생성일
2026-10-01
current_sales
180
prior_sales
150
yoy
20%을 보존하면 나중에 원천이 정정돼도 당시 보고서를 설명할 수 있습니다.
89장. 분석용 파생값과 원본 사실을 구분해야 한다#
원본 사실:
2026 Q3 매출
180파생값:
전년비
20%입니다.
파생값은:
전년도 값
계산 공식이 달라지면 바뀔 수 있습니다.
원본과 파생값을 같은 종류의 데이터처럼 다루면 변경 이유를 추적하기 어려워집니다.
90장. PERCENT_RANK 실습 체크리스트#
- 비교 집단은 누구인가?
- 퇴직자를 포함하는가?
- 미배정 직원을 포함하는가?
- 정렬 기준은 무엇인가?
- 동점자는 같은 순위를 가져야 하는가?
ORDER BY에 불필요한 동점 해제 열이 들어가 있지 않은가?- 계산 전에 필터링하는가 후에 필터링하는가?
- 전사 기준인가 부서 기준인가?
- 한 행짜리 집단의 결과를 확인했는가?
- 최고값 동점에서 1이 아닐 수 있음을 알고 있는가?
- 값을 0~1로 저장하는가 0~100으로 저장하는가?
- 기준일과 비교 인원을 남기는가?
91장. 전년 동기 비교 체크리스트#
- 기간당 정확히 한 행인가?
- 누락 기간이 존재하는가?
LAG N이 실제 기간 관계를 보장하는가?- 전년도 같은 기간을 키로 직접 연결할 수 있는가?
- 전년도 자료가 없는 경우를 구분하는가?
- 전년도 매출 0을 구분하는가?
- 미수집을 0으로 임의 변환하지 않는가?
- 증감액과 증감률을 함께 보는가?
- 기간 기준이 달력연도인가 회계연도인가?
- 원천 거래라면 먼저 기간 단위로 집계했는가?
- 조인 후 행 수가 예상과 같은가?
- 늦게 들어온 데이터로 과거 값이 변할 수 있는가?
92장. 분석 SQL에서 가장 위험한 세 가지 가정#
첫 번째:
이전 행
=
이전 기간두 번째:
NULL
=
0세 번째:
60% 백분위
=
정확히 60%의 사람이 나보다 낮음입니다.
세 가지 모두 겉으로는 자연스럽지만 정의를 확인하면 틀릴 수 있습니다.
93장. 문제를 풀 때는 계산 공식보다 비교 기준을 먼저 적자#
백분위 문제라면:
대상
재직 직원
기준
급여 오름차순
동점
같은 순위를 먼저 적습니다.
전년 동기 문제라면:
대상
부서별 분기 매출
비교
전년도 같은 분기
누락
비교 자료 없음으로 구분을 먼저 적습니다.
이렇게 하면 SQL 함수 선택이 훨씬 쉬워집니다.
94장. 핵심 정리#
PERCENT_RANK와 전년 동기 비교는 겉으로는 서로 다른 SQL 문제처럼 보입니다.
하지만 두 문제에는 같은 원칙이 있습니다.
비교 기준을 먼저 정의해야 한다.
급여 백분위에서는:
누가 비교 집단에 들어가는가?가 핵심입니다.
활성 직원 여섯 명의 급여가:
100
300
400
500
500
600이라면 급여 500의 공동 순위는 4입니다.
따라서:
PERCENT_RANK
=
(4 - 1)
/
(6 - 1)
=
0.6입니다.
하지만 500을 받는 직원이 한 명 더 추가되면 급여 자체는 그대로여도:
0.6
→
0.5로 바뀝니다.
이는 백분위가 절대값이 아니라 비교 집단에 의존하는 상대 지표이기 때문입니다.
또 ROW_NUMBER와 PERCENT_RANK의 정렬 기준을 혼동하면 동점 급여가 서로 다른 상대 위치를 가질 수 있습니다.
따라서:
순위 계산 기준
화면 출력 순서를 분리해서 생각하는 것이 좋습니다.
전년 동기 비교에서는 더 중요한 원칙이 있습니다.
몇 행 전
≠
몇 기간 전입니다.
2025 Q2와 2026 Q2가 빠진 데이터에서:
LAG(total_sales, 4)를 사용하면 2026 Q3의 비교값으로 2025 Q1이 선택될 수 있습니다.
그 결과:
잘못된 증가율
80%이 만들어집니다.
실제 전년도 같은 분기는:
2025 Q3
150이므로:
(180 - 150)
/
150
× 100
=
20%가 맞습니다.
따라서 전년 동기처럼 기간 자체에 의미가 있는 비교에서는:
같은 부서
전년도
같은 분기를 키로 직접 연결하는 방법이 더 명확할 수 있습니다.
또 다음 세 상태를 반드시 구분해야 합니다.
전년값 100
→ 정상 비교 가능
전년값 0
→ 증가율 분모 0
전년 행 없음
→ 비교 자료 없음이 셋을 모두 0이나 하나의 NULL 의미로 합치면 보고서의 뜻이 흐려집니다.
분석 SQL에서 가장 중요한 것은 함수를 많이 사용하는 것이 아닙니다.
이 숫자가 누구와 무엇을 비교해서 나온 값인지 설명할 수 있는가?
입니다.
PERCENT_RANK에서는 비교 집단을 설명할 수 있어야 하고, 전년비에서는 정확한 비교 기간을 설명할 수 있어야 합니다.
그리고 SQL을 실행하기 전에 항상 두 가지를 손으로 검산하는 습관이 좋습니다.
기대 결과 값
기대 결과 행 수이 두 가지가 맞지 않는다면 문법보다 먼저 비교 기준·그레인·누락 기간·동점 처리를 확인해야 합니다.