본문 바로가기
데이터베이스(SQL)/데이터베이스 개념

SQL JOIN 후 SUM이 커지는 이유: 상세 테이블 선집계로 중복 막기

by char_lie 2026. 10. 3.
반응형

주문 금액의 합은 250인데 상품과 결제 테이블을 조인하자 550이 됐다면, SUM보다 조인 결과의 행 단위를 먼저 확인해야 합니다. 하나의 주문에 상품과 결제가 여러 개씩 연결되면 주문 금액이 반복됩니다. 상세 테이블을 주문별로 먼저 집계한 뒤 조인하면 이 중복을 막을 수 있습니다.

여러 상세 테이블을 조인하면 같은 부모 행이 반복될 수 있습니다.

1. 작은 데이터로 합계가 늘어나는 상황 만들기

예제는 SQLite에서 실행했으며, 일반적인 SQL의 JOIN·GROUP BY·CTE를 사용합니다. 금액 100인 주문이 두 개 있다는 점도 확인해 두세요.

CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    total INTEGER
);
CREATE TABLE items (
    id INTEGER PRIMARY KEY,
    order_id INTEGER
);
CREATE TABLE payments (
    id INTEGER PRIMARY KEY,
    order_id INTEGER
);

INSERT INTO orders VALUES (1, 100), (2, 100), (3, 50);
INSERT INTO items VALUES (11, 1), (12, 1), (13, 2);
INSERT INTO payments VALUES (21, 1), (22, 1), (23, 2);

주문 1에는 상품 2개와 결제 2개가 있습니다. 주문 2에는 각각 1개, 주문 3에는 연결된 상세 행이 없습니다. 주문 테이블만 보면 100 + 100 + 50 = 250입니다.

SELECT SUM(o.total) AS order_total,
       SUM(DISTINCT o.total) AS distinct_total
FROM orders AS o
LEFT JOIN items AS i ON i.order_id = o.id
LEFT JOIN payments AS p ON p.order_id = o.id;
order_total | distinct_total
550         | 150

2. 조인 결과에서 주문 1이 네 번 나타나는 이유

주문 1의 상품과 결제는 주문 ID로만 연결됩니다. 상품 11은 결제 21·22와 각각 연결되고, 상품 12도 두 결제와 연결됩니다. 따라서 주문 1의 금액 100이 네 번 더해집니다.

order_id | item_id | payment_id | total
1        | 11      | 21         | 100
1        | 11      | 22         | 100
1        | 12      | 21         | 100
1        | 12      | 22         | 100
2        | 13      | 23         | 100
3        | NULL    | NULL       | 50

상품 개수 2와 결제 개수 2의 조합으로 네 행이 생긴 것입니다. JOIN이 잘못 계산한 것이 아니라, 상세 행의 조합마다 부모의 금액이 반복된 결과입니다. LEFT JOIN은 왼쪽 행을 보존하지만 한 행이 여러 행으로 늘어나는 것을 막아 주지는 않습니다.

3. SUM(DISTINCT 금액)이 정답이 아닌 이유

SUM(DISTINCT o.total)은 주문을 구분하지 않고 금액 값을 중복 제거합니다. 주문 1과 주문 2는 서로 다른 주문이지만 금액이 둘 다 100이므로 한 번만 더해져 150이 됩니다.

중복을 제거할 기준은 금액이 아니라 주문의 식별자와 집계 단위입니다. 상품·결제 집계가 필요 없다면 주문 테이블에서 바로 합계를 구하는 것이 가장 간단합니다. 상세 조건에 해당하는 주문만 고를 때는 의도에 맞는 EXISTS 조건도 검토할 수 있습니다.

4. 상세 테이블을 주문별 한 행으로 만든 뒤 조인하기

아래 쿼리는 각 상세 테이블을 order_id별로 집계합니다. 조인 직전 상품 요약과 결제 요약에는 주문별로 최대 한 행만 존재합니다.

상세 테이블을 같은 키로 먼저 집계하면 조인할 행의 단위를 맞출 수 있습니다.

WITH item_counts AS (
    SELECT order_id, COUNT(*) AS item_count
    FROM items
    GROUP BY order_id
),
payment_counts AS (
    SELECT order_id, COUNT(*) AS payment_count
    FROM payments
    GROUP BY order_id
),
order_metrics AS (
    SELECT o.id,
           o.total,
           COALESCE(i.item_count, 0) AS item_count,
           COALESCE(p.payment_count, 0) AS payment_count
    FROM orders AS o
    LEFT JOIN item_counts AS i ON i.order_id = o.id
    LEFT JOIN payment_counts AS p ON p.order_id = o.id
)
SELECT SUM(total) AS order_total,
       SUM(item_count) AS item_count,
       SUM(payment_count) AS payment_count
FROM order_metrics;
order_total | item_count | payment_count
250         | 3          | 3

주문 3은 상세 요약과 연결되지 않아 개수가 NULL이 됩니다. 여기서는 상세 행이 없다는 의미를 0으로 표시하기 위해 COALESCE를 사용했습니다. 주문 금액은 원래 주문별 한 번씩만 집계됩니다.

5. 적용하기 전에 점검할 예외

  • 집계 키: 주문 단위 결과가 필요하면 각 상세 요약도 주문 단위여야 합니다. 주문·상품별로 집계한 요약은 주문당 여러 행일 수 있습니다.
  • 조건의 위치: 성공 결제만 세려면 결제 요약 안에서 해당 조건을 먼저 적용합니다. 주문을 제외할 조건과 상세 행만 제외할 조건을 구분하세요.
  • 다른 연결 관계: 특정 결제가 특정 상품에 대응한다면 실제 관계 키로 조인해야 합니다. 주문 ID만으로 모든 조합을 만드는 예제와 의미가 다릅니다.
  • 빈 전체 집합: 주문이 한 건도 없으면 일반적인 SUM 결과는 NULL입니다. 화면에서 0을 요구할 때만 최종 합계에 COALESCE를 적용합니다.

성능은 데이터 크기·인덱스·실행 계획에 따라 달라집니다. 선집계라는 형태만으로 항상 빨라진다고 단정하지 말고, 먼저 결과의 정확성을 확인한 뒤 실제 실행 계획을 살펴보세요.

핵심 정리: 조인 뒤 합계가 커지면 부모 키별 행 수를 세어 보세요. 상세 테이블의 집계 단위를 맞춘 후 조인하고, 같은 금액을 가진 서로 다른 부모 행을 값 기준 DISTINCT로 지우지 않는 것이 중요합니다.

JOIN의 기본 결과와 조건 위치가 헷갈린다면 INNER JOIN·LEFT JOIN 결과 비교를 먼저 확인하면 이번 집계 문제를 이해하기 쉽습니다.

반응형

댓글