🌈 프로그래밍/TIL
Django ORM aggregate Sum이 NULL을 반환할 때
수구리
2025. 12. 19. 15:33
반응형
오늘 코드 리뷰 받다가 알게 된 내용 정리.
발단
프라임타임 점수 계산 로직에서 이렇게 썼었다:
def calculate_score_for_stadium(self, stadium_id: int) -> int:
result = (
FixedMatch.objects.prime_time_weekday()
.for_stadium(stadium_id)
.aggregate(total_score=Sum(self._build_score_case_expression()))
)
return result["total_score"] or 0 # <- or 0 필요한가?
_build_score_case_expression()에서 이미 default=Value(0)을 설정했는데, or 0이 필요한지 의문이 들었다.
def _build_score_case_expression(self) -> Case:
when_clauses = [
When(match_time__hour=hour, then=Value(score))
for hour, score in self.SCORE_BY_HOUR.items()
]
return Case(
*when_clauses,
default=Value(0), # <- 기본값 0 설정했는데?
output_field=IntegerField(),
)
왜 or 0이 필요한가?
Case의 default는 "행(row) 단위"
Case의 default=Value(0)는 각 행에 대해 조건에 맞지 않을 때 0을 반환한다.
# 3개 행이 있을 때
# row 1: hour=19 → score=1
# row 2: hour=20 → score=2
# row 3: hour=18 → score=0 (default)
# Sum = 3
문제는 "행이 없을 때"
해당 Stadium에 프라임타임 고정매치가 없으면 QuerySet이 비어있다.
# 해당 Stadium에 매칭되는 행이 0개
FixedMatch.objects.prime_time_weekday().for_stadium(stadium_id)
# → 빈 QuerySet
# Sum()을 빈 QuerySet에 적용하면?
.aggregate(total_score=Sum(...))
# → {"total_score": None} # 0이 아니라 None!
SQL 표준 동작
이건 SQL 표준이다. SUM()을 빈 결과셋에 적용하면 NULL을 반환한다.
-- 결과가 0개인 경우
SELECT SUM(score) FROM fixed_match WHERE 1=0;
-- 결과: NULL (0이 아님)
Django도 이 동작을 그대로 따른다.
해결책: Coalesce 사용
or 0도 동작하지만, Coalesce가 의도를 더 명확히 표현한다.
from django.db.models.functions import Coalesce
def calculate_score_for_stadium(self, stadium_id: int) -> int:
result = (
FixedMatch.objects.prime_time_weekday()
.for_stadium(stadium_id)
.aggregate(
total_score=Coalesce(Sum(self._build_score_case_expression()), 0)
)
)
return result["total_score"] # or 0 불필요
Coalesce vs or 0
| 방식 | 처리 위치 | 장점 |
|---|---|---|
or 0 |
Python | 간단함 |
Coalesce |
DB | 의도 명확, SQL에서 처리 |
Coalesce는 SQL의 COALESCE() 함수로 변환된다:
SELECT COALESCE(SUM(score), 0) AS total_score FROM ...
결론
Sum()은 빈 QuerySet에서None을 반환함 (SQL 표준)Case의default는 행 단위로만 적용됨Coalesce로 NULL 처리를 명시적으로 하는 게 좋음
# Before
.aggregate(total_score=Sum(...))
return result["total_score"] or 0
# After
.aggregate(total_score=Coalesce(Sum(...), 0))
return result["total_score"]
참고: 다른 집계 함수들도 마찬가지
Count()만 예외적으로 빈 결과셋에서 0을 반환한다.
# 빈 QuerySet에서
Sum(...) → None
Avg(...) → None
Max(...) → None
Min(...) → None
Count(...) → 0 # 예외!반응형