본문 바로가기
빌드노트 검색

빌드노트

PostgreSQL 누적 합계가 같은 날짜의 두 행에서 함께 뛴다

같은 날짜의 행에 같은 누적 합계가 붙는 이유는 기본 RANGE 프레임이 동률 행까지 포함하기 때문이다. PostgreSQL 18.6에서 입력 순서를 뒤집어 비교하고, ROWS의 행 범위와 고유 보조 키가 각각 해결하는 문제를 구분한다.

PostgreSQL에서 sum(amount) OVER (ORDER BY day)를 썼는데 같은 날짜의 여러 행에 같은 누적 합계가 찍힌다면, 생략된 프레임을 확인한다. 기본 프레임은 현재 행에서 멈추지 않고, 정렬값이 같은 행까지 함께 포함한다. 행별 누적을 원하면 ROWS로 범위를 지정하고 OVER 안의 정렬에도 고유한 보조 키를 넣어야 한다.

자기 쿼리에서 먼저 볼 곳은 맨 끝의 ORDER BY가 아니라 sum(...) OVER (...) 안이다. 날짜까지의 합계를 각 행에 표시하려는지, 같은 날짜 안에서도 한 행씩 더한 값을 표시하려는지에 따라 올바른 식이 달라진다.

같은 날짜의 두 행에 왜 60이 붙을까

프레임(frame)은 현재 행의 합계를 구할 때 포함하는 행의 범위다. ORDER BY day를 넣고 프레임을 생략하면 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW가 적용된다. 이때 CURRENT ROW의 끝 경계는 같은 날짜를 가진 마지막 행까지다. 윈도 정렬에서 같은 값으로 취급되는 행을 동률 행(peer)이라고 한다. PostgreSQL 18 윈도 함수 문서 (새 탭에서 열림)는 정렬값이 같은 뒤쪽 행도 기본 프레임에 들어간다고 설명한다.

PostgreSQL에 연결된 SQL 콘솔에서 아래 SELECT로 기본 합계와 행별 합계를 비교할 수 있다. 테이블을 만들 필요는 없다. id는 네 행에서 모두 다르며, 이 예제에서는 같은 날짜 안에서 작은 ID부터 누적하기로 정했다.

WITH entries(id, day, amount) AS (
  VALUES
    (1, DATE '2026-10-20', 10),
    (2, DATE '2026-10-21', 20),
    (3, DATE '2026-10-21', 30),
    (4, DATE '2026-10-22', 40)
)
SELECT id, day, amount,
       sum(amount) OVER (ORDER BY day) AS default_sum,
       sum(amount) OVER (
         ORDER BY day, id
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS row_sum,
       sum(amount) OVER (ORDER BY day, id) AS keyed_default_sum
FROM entries
ORDER BY day, id;

전용 PostgreSQL 18.6 컨테이너에서 실행한 결과다. 콘솔의 열 간격 대신 CSV로 표시했다.

id,day,amount,default_sum,row_sum,keyed_default_sum
1,2026-10-20,10,10,10,10
2,2026-10-21,20,60,30,30
3,2026-10-21,30,60,60,60
4,2026-10-22,40,100,100,100

default_sum에서 ID 2와 3은 모두 10 + 20 + 30 = 60이다. ORDER BY day에서는 둘이 동률이므로 두 행의 프레임이 같은 곳에서 끝난다. 두 행을 하나로 줄인 것은 아니다. 출력에는 네 행이 그대로 남아 있다.

row_sum은 day, id 순서에서 처음부터 현재 행까지만 더한다. ID 2에서는 10 + 20 = 30, ID 3에서는 60이다. 별도 실행에서 날짜 정렬에 기본 RANGE를 명시해도 기본 합계와 같은 10, 60, 60, 100이 나왔다. 프레임 구문 문서 (새 탭에서 열림)에 따르면 ROWS의 CURRENT ROW는 현재 행 자체를 뜻한다.

마지막 keyed_default_sum도 30, 60으로 나뉜다. 프레임은 여전히 기본 RANGE지만, day, id 조합이 유일해 다른 행이 동률로 묶이지 않기 때문이다. 보조 키를 추가하면 행 순서뿐 아니라 무엇을 동률로 볼지도 바뀐다. 날짜가 같은 행에 같은 날짜 누계를 표시하려던 조회라면 이 변경은 의도를 바꾼다.

ROWS만 바꾸면 어느 행을 먼저 더할지는 남는다

ROWS를 쓰더라도 ORDER BY day만으로는 같은 날짜의 두 행 중 어느 쪽이 먼저인지 정하지 못한다. 이를 확인하려면 앞 쿼리의 WITH entries(...) AS (...) 부분은 두고, 그 아래 SELECT 전체를 다음으로 바꾼다. 윈도 합계는 하나만 남겨 날짜만 정렬하는 조건을 비교한다.

SELECT id, day, amount,
       sum(amount) OVER (
         ORDER BY day
         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS rows_day_sum
FROM entries
ORDER BY day, id;

이 실행에서 ID 2와 3의 합계는 각각 30, 60이었다. 이어서 VALUES의 네 행을 ID 4, 3, 2, 1 순으로 나열하고 같은 SELECT를 실행했다. 이번에는 ID 2가 60, ID 3이 40이었다. ID 3의 금액 30이 먼저 더해져 10 + 30 = 40이 되고, ID 2의 금액 20이 나중에 더해진 결과다.

두 실행 모두 맨 끝의 ORDER BY day, id 때문에 화면에는 ID 2가 ID 3보다 먼저 나왔다. 출력 순서를 정렬해도 이미 계산한 윈도 합계의 순서는 바뀌지 않는다. 반대로 OVER 안의 정렬만으로 최종 출력 순서를 보장할 수도 없다. 계산 순서와 표시 순서는 각 위치에 적는다.

입력 순서를 뒤집으면 언제나 이 값으로 바뀐다는 뜻은 아니다. 이번에 관찰한 두 결과이며, 같은 쿼리를 반복해 우연히 같은 값이 나와도 동률 순서가 보장되지는 않는다. SELECT의 WINDOW 절 문서 (새 탭에서 열림)도 ROWS에서 정렬이 행을 유일하게 구분하지 못할 때 결과를 예측할 수 없다고 경고한다.

처음의 day, id 정렬을 쓴 비교 쿼리는 VALUES를 역순으로 배치해도 결과가 모두 같았다. 실제 조회에 적용할 때는 원하는 처리 순서를 먼저 정한다. 발생 시각 순서가 필요하면 해당 시각을 정렬에 넣고, 시각도 같은 행을 구분할 고유 키를 뒤에 둔다. ID 오름차순이 업무상 발생 순서와 같다고 가정하지 않는다. PARTITION BY가 있다면 그 구역 안에서 정렬 키 조합이 행을 유일하게 구분해야 한다.

같은 날짜까지의 합계를 원하면 날짜를 기준으로 동률을 포함하는 기본 프레임이 맞다. 행별 누적을 원하면 ORDER BY day, id와 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW를 함께 적는다. 그래도 기대값과 다르면 윈도 계산에 들어오는 실제 행의 날짜·보조 키·금액을 같이 조회해 중복과 정렬 기준부터 확인한다.

이번 실험은 NULL 없는 날짜·양의 정수 금액·고유 ID를 가진 합성 네 행의 SELECT다. 실제 매출 데이터나 운영 DB는 사용하지 않았다. 시간대 변환, NULL·음수 금액, 동시 데이터 변경과 실제 테이블의 성능은 검증하지 않았다.

← 전체 글 보기