SQL 백분위와 전년 동기 비교: PERCENT_RANK·LAG·누락 기간 처리


1장. 전년 대비 80% 성장이라고 했는데 실제로는 20%였다#

개발부의 분기별 매출이 다음과 같다고 하겠습니다.

2025 Q1
100

2025 Q3
150

2025 Q4
200

2026 Q1
120

2026 Q3
180

2025년 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:

150

2026 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

20

2026 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 실습 체크리스트#

  1. 비교 집단은 누구인가?
  2. 퇴직자를 포함하는가?
  3. 미배정 직원을 포함하는가?
  4. 정렬 기준은 무엇인가?
  5. 동점자는 같은 순위를 가져야 하는가?
  6. ORDER BY에 불필요한 동점 해제 열이 들어가 있지 않은가?
  7. 계산 전에 필터링하는가 후에 필터링하는가?
  8. 전사 기준인가 부서 기준인가?
  9. 한 행짜리 집단의 결과를 확인했는가?
  10. 최고값 동점에서 1이 아닐 수 있음을 알고 있는가?
  11. 값을 0~1로 저장하는가 0~100으로 저장하는가?
  12. 기준일과 비교 인원을 남기는가?

91장. 전년 동기 비교 체크리스트#

  1. 기간당 정확히 한 행인가?
  2. 누락 기간이 존재하는가?
  3. LAG N이 실제 기간 관계를 보장하는가?
  4. 전년도 같은 기간을 키로 직접 연결할 수 있는가?
  5. 전년도 자료가 없는 경우를 구분하는가?
  6. 전년도 매출 0을 구분하는가?
  7. 미수집을 0으로 임의 변환하지 않는가?
  8. 증감액과 증감률을 함께 보는가?
  9. 기간 기준이 달력연도인가 회계연도인가?
  10. 원천 거래라면 먼저 기간 단위로 집계했는가?
  11. 조인 후 행 수가 예상과 같은가?
  12. 늦게 들어온 데이터로 과거 값이 변할 수 있는가?

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을 실행하기 전에 항상 두 가지를 손으로 검산하는 습관이 좋습니다.

기대 결과 값

기대 결과 행 수

이 두 가지가 맞지 않는다면 문법보다 먼저 비교 기준·그레인·누락 기간·동점 처리를 확인해야 합니다.

이 페이지의 목차