GROUP BY와 TRUNCATE 때문에 부가세 합계가 맞지 않았던 이유
한 줄 요약: 금액 대사에서 공급가액이나 수수료 합계는 맞는데 부가세만 몇 원씩 어긋난다면, 데이터 누락보다 먼저 절사 기준 단위가 같은지를 확인해야 한다.
정산 데이터를 대사하다 보면 꽤 애매한 차이를 만날 때가 있다.
예를 들어 원천 데이터와 정산 데이터의 수수료 합계는 정확히 일치한다. 조회 조건도 맞고, 조인 결과도 이상해 보이지 않는다. 그런데 부가세 합계만 몇 원 단위로 차이가 난다.
처음에는 자연스럽게 데이터 누락을 의심했다. 특정 일자의 데이터가 빠졌거나, 조인 조건이 달라서 일부 row가 중복 또는 제외되었을 가능성을 먼저 봤다.
그런데 원인을 따라가 보니 문제는 데이터 개수나 조인 조건이 아니었다. 핵심은 부가세를 계산한 뒤 언제 TRUNCATE 했는가였다.
처음에 헷갈렸던 지점
수수료의 10%를 부가세로 계산한다고 하면 보통 아래처럼 생각하기 쉽다.
TRUNCATE(SUM(fee) * 0.1, 0)
전체 수수료를 먼저 합산하고, 그 합계에 10%를 곱한 뒤 소수점 이하를 버리는 방식이다.
겉으로 보면 이것도 "수수료의 10% 부가세"이고, 아래 방식도 같은 의미처럼 보인다.
SUM(TRUNCATE(fee * 0.1, 0))
또는 정산일자 같은 그룹 기준이 있다면 이런 형태가 될 수 있다.
SUM(TRUNCATE(SUM(fee) * 0.1, 0))
하지만 이 계산들은 같은 결과를 보장하지 않는다. 수수료 합계가 같더라도 부가세 합계는 달라질 수 있다.
이 차이는 SUM과 TRUNCATE 중 무엇을 먼저 적용했는지에서 나온다.
예제 데이터로 확인해보기
아래처럼 정산일자별 수수료가 있다고 가정해보자.
| 정산일자 | 수수료 |
|---|---|
| 2026-04-01 | 109 |
| 2026-04-02 | 109 |
| 2026-04-03 | 109 |
전체 수수료 합계는 327이다.
109 + 109 + 109 = 327
이제 부가세를 어떻게 계산하느냐에 따라 결과가 어떻게 달라지는지 보자.
방식 1. 전체 합산 후 한 번만 절사하기
먼저 전체 수수료를 합산한 뒤 부가세를 계산한다.
327 * 0.1 = 32.7
TRUNCATE(32.7) = 32
SQL로 쓰면 다음과 같다.
SELECT TRUNCATE(SUM(fee) * 0.1, 0) AS vat
FROM fee_sample;
결과는 32가 된다.
이 방식은 월 전체, 기간 전체, 조회 결과 전체를 하나의 계산 단위로 보고 부가세를 한 번만 절사한다.
방식 2. 그룹별로 절사한 뒤 다시 합산하기
이번에는 정산일자별로 부가세를 먼저 계산하고 절사한다고 가정해보자.
2026-04-01: 109 * 0.1 = 10.9 -> TRUNCATE = 10
2026-04-02: 109 * 0.1 = 10.9 -> TRUNCATE = 10
2026-04-03: 109 * 0.1 = 10.9 -> TRUNCATE = 10
그리고 절사된 부가세를 다시 합산한다.
10 + 10 + 10 = 30
SQL로 표현하면 다음과 같다.
SELECT SUM(vat) AS vat
FROM (
SELECT settle_day,
TRUNCATE(SUM(fee) * 0.1, 0) AS vat
FROM fee_sample
GROUP BY settle_day
) T;
결과는 30이다.
같은 수수료 데이터인데도 최종 부가세가 달라졌다.
전체 합산 후 절사: 32
정산일자별 절사 후 합산: 30
차이: 2
처음에는 이 차이가 이상해 보일 수 있다. 하지만 계산 순서를 보면 자연스러운 결과다.
왜 차이가 누적될까?
TRUNCATE는 소수점 이하를 버린다. 반올림이 아니라 버림이다.
예를 들어 10.9를 TRUNCATE하면 10이 된다. 이때 사라진 0.9는 이후 계산에서 복구되지 않는다.
절사를 전체 결과에 한 번만 적용하면 버려지는 소수점도 한 번만 발생한다.
전체 합산 -> 부가세 계산 -> 절사
반대로 그룹별로 먼저 절사하면 각 그룹마다 소수점이 버려진다.
그룹 A -> 부가세 계산 -> 절사
그룹 B -> 부가세 계산 -> 절사
그룹 C -> 부가세 계산 -> 절사
이후 절사된 값 합산
그룹 수가 많아질수록 버려진 소수점도 여러 번 생긴다. 그래서 최종 합계 차이가 몇 원에서 몇십 원, 데이터가 많으면 그 이상으로 커질 수 있다.
여기서 중요한 점은 GROUP BY 자체가 문제는 아니라는 것이다. 문제는 그룹별 계산 결과에 절사를 먼저 적용했는지, 아니면 전체 합계에 절사를 한 번만 적용했는지다.
실제 대사에서 봐야 하는 기준
대사 쿼리를 작성할 때는 "최종 합계가 같은가"만 보면 부족하다.
특히 금액 데이터는 저장 시점에 이미 특정 기준으로 계산되어 있을 수 있다. 예를 들어 정산 테이블의 부가세가 아래 흐름으로 만들어졌다고 해보자.
정산일자별 수수료 합산
-> 정산일자별 부가세 계산
-> 정산일자별 부가세 절사
-> 월 조회 시 절사된 부가세 합산
이 경우 원천 데이터를 대사할 때도 같은 단위로 계산해야 한다.
아래처럼 기간 전체를 한 번에 합산해서 부가세를 계산하면 저장된 정산 데이터와 기준이 달라진다.
SELECT TRUNCATE(SUM(fee) * 0.1, 0) AS vat
FROM source_data
WHERE settle_day BETWEEN 20260401 AND 20260430;
정산 데이터가 정산일자별 절사 기준으로 생성되었다면 대사 쿼리도 다음처럼 작성하는 편이 맞다.
SELECT SUM(vat) AS vat
FROM (
SELECT settle_day,
TRUNCATE(SUM(fee) * 0.1, 0) AS vat
FROM source_data
WHERE settle_day BETWEEN 20260401 AND 20260430
GROUP BY settle_day
) T;
대사의 목적은 내가 생각한 이상적인 계산식을 적용하는 것이 아니라, 실제 데이터가 생성된 기준과 같은 방식으로 다시 계산해 비교하는 것이다.
확인용 SQL 예제
아래 예제 테이블로 직접 차이를 확인할 수 있다.
CREATE TABLE fee_sample (
settle_day INT,
fee DECIMAL(10, 2)
);
INSERT INTO fee_sample VALUES
(20260401, 109.00),
(20260402, 109.00),
(20260403, 109.00);
전체 합산 후 한 번만 절사하면 결과는 32다.
SELECT TRUNCATE(SUM(fee) * 0.1, 0) AS vat
FROM fee_sample;
정산일자별로 먼저 절사하고 합산하면 결과는 30이다.
SELECT SUM(vat) AS vat
FROM (
SELECT settle_day,
TRUNCATE(SUM(fee) * 0.1, 0) AS vat
FROM fee_sample
GROUP BY settle_day
) T;
이 예제는 일부러 단순하게 만든 데이터지만, 실제 정산 데이터에서도 같은 문제가 생긴다. 오히려 실제 데이터는 그룹 기준이 더 많고 금액도 다양하기 때문에 원인 파악이 더 늦어질 수 있다.
대사 쿼리를 작성할 때의 체크리스트
금액 대사에서 몇 원 단위 차이가 난다면 아래 항목을 순서대로 확인하는 것이 좋다.
- 원천 데이터와 정산 데이터의 조회 기간이 같은가
- 조인 기준 때문에 row가 중복되거나 누락되지 않았는가
- 공급가액, 수수료 같은 원금 성격의 합계는 일치하는가
- 부가세나 수수료처럼 계산된 금액의 절사 또는 반올림 기준이 같은가
TRUNCATE,ROUND,FLOOR같은 함수가 row 단위, 그룹 단위, 전체 단위 중 어디에 적용되었는가- 실제 저장된 데이터가 어떤 단위로 생성되었는가
처음부터 절사 기준을 의심하라는 뜻은 아니다. 다만 원금 합계는 맞는데 계산 금액만 어긋난다면, 데이터 누락만 계속 추적하기보다 계산 단위를 함께 봐야 한다.
주의할 점
금액 계산에서는 DECIMAL 같은 고정 소수점 타입을 사용하는 것이 좋다. FLOAT, DOUBLE은 이진 부동소수점 표현 방식 때문에 예상하지 못한 미세 오차가 생길 수 있다.
다만 이번 문제의 핵심은 부동소수점 오차가 아니다. DECIMAL을 사용하더라도 절사 시점이 다르면 결과는 달라질 수 있다.
또 하나 주의할 점은 "어느 방식이 무조건 맞다"라고 단정하기 어렵다는 것이다.
월 전체 기준으로 세금을 한 번 계산해야 하는 업무도 있을 수 있고, 거래별 또는 일자별로 계산된 세금을 합산해야 하는 업무도 있을 수 있다. 중요한 것은 쿼리 작성자가 임의로 기준을 정하는 것이 아니라, 실제 정산 정책과 저장 로직의 기준을 따라야 한다는 점이다.
정리
이번 문제의 원인은 데이터 누락이나 조인 오류가 아니었다.
원인은 부가세 계산에서 TRUNCATE를 적용하는 기준 단위가 서로 달랐기 때문이었다.
아래 두 계산은 같은 결과를 보장하지 않는다.
-- 전체 합산 후 절사
TRUNCATE(SUM(fee) * 0.1, 0)
-- 그룹별 절사 후 합산
SUM(TRUNCATE(SUM(fee) * 0.1, 0))
금액 대사에서는 최종 합계만 맞추는 것보다, 실제 데이터가 어떤 단위로 계산되고 저장되었는지를 맞추는 것이 더 중요하다.
공급가액이나 수수료는 맞는데 부가세만 몇 원씩 차이가 난다면, GROUP BY 기준과 TRUNCATE 적용 시점을 먼저 의심해볼 만하다.