마케팅 데이터 분석

한 행이 무엇을 뜻하는지 정하는 데이터 모델

주문 3건이 상품 행 5개로 펼쳐질 때 집계 단위가 어떻게 바뀌는지 확인합니다. 중복 없는 키만으로 매출 합계를 보장할 수 없는 이유와 SUM DISTINCT의 함정을 합성 데이터·SQLite 예제로 재현합니다.

합성 주문의 매출은 250,000원입니다. 상품 정보를 붙이려고 테이블을 조인한 뒤 주문 금액을 더했더니 400,000원이 됐습니다. SUM(DISTINCT 금액)으로 바꾸자 이번에는 150,000원이 나옵니다. 키 중복과 합계만 번갈아 확인해서는 원인을 설명하기 어렵습니다. 결과의 한 행이 무엇을 나타내는지부터 봐야 합니다.

아래 예제는 한 상점의 주문과 상품 행을 단순화한 합성 데이터입니다. 금액은 원화의 상품 금액이고, 할인은 상품 행 단가에 이미 반영됐다고 가정합니다. 세금·배송비·환불은 별도로 더하지 않으므로 주문 금액과 상품 행 금액의 합이 같아야 합니다.

한 행의 의미를 먼저 정해야 하는 이유

분석용 데이터를 모아 두는 데이터 웨어하우스에서 grain이라고 부르는 것은 한 행이 나타내는 업무상 측정 단위입니다. ‘주문 한 건’, ‘주문 안의 상품 행 하나’, ‘고객의 하루 활동 집계’처럼 그 행을 읽는 사람이 무엇을 세고 있는지 설명할 수 있어야 합니다.

Ralph Kimball의 2008년 글도 grain을 먼저 선언하고, 그 단위에 맞는 키와 측정값을 정하도록 설명합니다. 키 목록을 나열하는 것만으로 한 행의 의미가 정의되지는 않습니다. Kimball Group의 Fact Tables

테이블 또는 결과한 행의 의미식별 기준 예시더할 수 있는 값의 예시
orders주문 한 건주문 ID같은 기준의 주문 금액
order_items주문 안의 상품 행 하나주문 ID + 행 번호수량 × 해당 행의 단가
order_item_totals상품 행을 주문별로 집계한 한 건주문 ID주문별 상품 금액 합계

‘고객 ID가 있다’는 설명만으로 고객당 한 행인 테이블이라고 판단할 수는 없습니다. 주문 테이블에도 고객 ID가 반복될 수 있기 때문입니다. 행의 의미, 키가 유일한 범위, 지표를 합산할 수 있는 범위를 함께 적는 것이 좋습니다.

주문 수·상품 행 수·상품 수량은 어떻게 다른가

주문은 세 건이고 고객은 두 명입니다. u1이 두 번 주문했습니다.

주문 ID고객 ID주문 금액
o601u1100,000원
o602u2100,000원
o603u150,000원

상품 행은 다음과 같습니다. SKU는 상품을 구분하는 코드이고, 행 번호는 주문 안의 각 행을 구분합니다.

주문 ID행 번호SKU수량행 단가수량 × 단가
o6011A140,000원40,000원
o6012B230,000원60,000원
o6021A250,000원100,000원
o6031C130,000원30,000원
o6032C120,000원20,000원

o603에는 같은 SKU C가 다른 단가로 두 행에 있습니다. 이 예제에서는 조건이 다른 별도 상품 행이라고 정했습니다. (주문 ID, SKU)만으로 한 행을 남기면 정상적인 상품 행이 사라집니다. 상품 행의 키는 (주문 ID, 행 번호)입니다.

서로 다른 단위의 개수를 같은 구매 수로 부르지 않습니다주문 한 건씩3건주문 안의 상품 행5행수량을 더한 상품 개수7개구매자는 u1과 u2, 두 명입니다. 한 사람이 여러 번 주문할 수 있습니다.합성 데이터 · 주문 금액 합계 250,000원
상품 행 수를 세는 COUNT와 수량을 더하는 SUM은 다른 질문에 답합니다.

상품 수량은 1 + 2 + 2 + 1 + 1 = 7개입니다. 상품 행은 5개이고, SKU 종류는 A·B·C 세 가지입니다. 하나의 숫자가 맞는지를 확인하려면 먼저 어떤 단위를 세기로 했는지 정해야 합니다.

상품 행과 조인한 뒤 주문 금액을 더하면 어떻게 되나

주문 o601을 상품 테이블에 연결하면 두 행이 나옵니다. 두 행 모두 주문 o601에 속하므로 주문 총액 100,000원이 각각 표시됩니다. 원천에 주문이 중복 저장된 상황과는 구분해야 합니다. 조인 결과가 상품 행 단위로 펼쳐진 것입니다.

상품 행으로 펼친 결과에 주문 총액이 반복됩니다주문 o601100,000원o601 · 상품 행 1 · SKU A주문 총액 100,000원 / 행 금액 40,000원o601 · 상품 행 2 · SKU B주문 총액 100,000원 / 행 금액 60,000원합성 예제의 o601만 표시 · 주문 총액 열과 상품 행 금액 열의 집계 단위를 구분
조인 결과를 상품별로 분석할 때는 상품 행에 해당하는 금액과 수량을 사용합니다.

전체 주문을 같은 방식으로 연결하고 주문 금액을 더하는 쿼리입니다.

SELECT
    COUNT(*) AS joined_rows,
    COUNT(DISTINCT o.order_id) AS distinct_orders,
    SUM(o.order_amount_krw) AS revenue
FROM orders AS o
JOIN order_items AS i ON o.order_id = i.order_id;

결과는 5행, 고유 주문 3건, 합계 400,000원입니다. 주문 건수에는 DISTINCT를 사용했지만 주문 금액의 반복까지 해결되지는 않았습니다. o601과 o603은 각각 상품 행이 두 개라 주문 금액도 두 번씩 더해졌습니다.

반복 합계 = 100,000 × 2 + 100,000 × 1 + 50,000 × 2 = 400,000원

SUM DISTINCT가 주문 중복을 해결하지 못하는 이유

SUM(DISTINCT order_amount_krw)는 금액 값의 중복을 제거합니다. 주문의 중복을 제거하는 연산과는 다릅니다. 예제에는 서로 다른 100,000원 주문 o601과 o602가 있는데, 금액만 고유화하면 100,000원을 한 번만 더합니다.

금액 값만 고유화한 합계 = 100,000 + 50,000 = 150,000원

SQLite의 집계 함수 문서도 DISTINCT가 집계 함수에 전달되는 중복 값을 제거하는 방식이라고 설명합니다. 어느 값에 DISTINCT를 적용했는지가 중요합니다. SQLite 집계 함수의 DISTINCT

상품 행으로 펼친 결과에서 (주문 ID, 행 번호)는 여전히 유일합니다. 중복 없는 행 식별자가 있어도 주문 총액을 상품 행 단위로 합산하면 잘못된 값이 나옵니다. 키의 유일성과 측정값의 집계 단위는 별도로 확인해야 합니다.

주문 단위로 먼저 집계한 뒤 연결하기

주문 전체의 매출만 필요하다면 상품 테이블을 붙이지 않고 주문 테이블에서 바로 합산하는 방법이 가장 명확합니다.

SELECT SUM(order_amount_krw) AS revenue
FROM orders;

주문별 상품 수량이나 행 수도 함께 필요하다면 상품을 주문당 한 행으로 먼저 집계한 결과를 연결할 수 있습니다. 예제에서는 조회할 때 다음 집계를 계산하도록 order_item_totals라는 뷰를 정의했습니다.

SELECT
    order_id,
    COUNT(*) AS item_lines,
    SUM(quantity) AS item_units,
    SUM(quantity * unit_price_krw) AS item_revenue_krw
FROM order_items
GROUP BY order_id;

이 결과에는 주문마다 한 행이 있습니다. 주문 테이블과 연결해도 주문 금액이 상품 행 수만큼 반복되지 않습니다. 다음 쿼리의 결과는 250,000원입니다.

SELECT SUM(o.order_amount_krw) AS revenue
FROM orders AS o
JOIN order_item_totals AS t ON o.order_id = t.order_id;
같은 원본에서 집계 단위와 식별 기준만 달라진 결과주문 테이블 합계250,000원상품 조인 후 합계400,000원금액 DISTINCT 합계150,000원주문별 집계 후 조인250,000원검증된 주문 합계는 250,000원입니다. 합계가 줄었다고 항상 더 정확한 것은 아닙니다.합성 데이터 · SQLite에서 각 쿼리 실행 · 막대 길이는 금액에 비례
사전 집계는 결과의 단위를 맞추는 방법입니다. 상품별 분석에는 원래 상품 행을 사용합니다.

주문별로 집계하면 SKU별 상세 정보는 그 결과에 남지 않습니다. 상품별 매출이 필요하다면 원래 상품 테이블에서 수량 × 단가를 SKU별로 더합니다. 예제의 SKU별 금액은 A 140,000원, B 60,000원, C 50,000원이고 합계는 250,000원입니다.

분석 질문에 필요한 단위가 무엇인지에 따라 사용할 테이블이 달라집니다. 모든 테이블을 미리 한 장으로 합쳐 놓는 것만으로 여러 질문에 정확하게 답할 수 있는 데이터 모델이 만들어지지는 않습니다.

키와 제약 조건으로 어디까지 검사할 수 있나

예제의 주문 테이블은 주문 ID를 기본키로, 상품 테이블은 주문 ID와 행 번호를 복합 기본키로 사용합니다. 상품 행은 원래 주문을 참조하도록 외래키를 둡니다. 핵심 구조는 다음과 같습니다.

CREATE TABLE order_items (
    order_id TEXT NOT NULL,
    line_no INTEGER NOT NULL,
    sku TEXT NOT NULL,
    quantity INTEGER NOT NULL,
    unit_price_krw INTEGER NOT NULL,
    PRIMARY KEY (order_id, line_no),
    FOREIGN KEY (order_id) REFERENCES orders(order_id)
);

다운로드 예제의 전체 schema.sql에는 양의 정수 수량과 비음수 정수 금액을 확인하는 조건도 있습니다. SQLite의 외래키는 연결마다 명시적으로 켜고 실제 활성화됐는지 확인합니다. 문서도 런타임 설정을 확인하도록 안내합니다. SQLite 외래키 지원

제약 조건만으로 충분하지 않은 검증도 있습니다. 상품 행의 외래키는 존재하지 않는 주문을 가리키는 행을 막아 주지만, 모든 주문에 상품 행이 적어도 하나 있는지까지 보장하지는 않습니다. 원천에서 상품 행이 누락됐는지와 주문·상품 금액이 맞는지는 별도로 대사합니다.

예제는 주문을 왼쪽에 두고 상품 집계 결과를 연결해 누락을 확인합니다. 상품 행이 없는 주문을 0원 상품으로 간주하지 않습니다. 실제로 단가가 0원인 상품 행이 있는 경우와 정보가 빠진 경우는 다른 상태입니다.

측정값이 어느 키에 의존하는지 확인하기

이 예제에서는 주문 ID가 정해지면 해당 주문의 총액도 하나로 정해집니다. 주문 ID → 주문 금액이라는 관계로 표현할 수 있습니다. 상품 행으로 펼친 결과에 주문 금액을 붙여도 그 금액이 상품 행의 금액으로 바뀌지는 않습니다.

주문 o의 금액을 v(o), 연결된 상품 행 수를 m(o)라고 하면 잘못 합산한 금액은 다음처럼 표현됩니다.

조인 후 주문 금액 합계 = Σ m(o) × v(o)
원래 주문 금액 합계   = Σ v(o)

상품 행 수가 주문마다 다르므로 전체 행 수의 비율로 한 번 나눠서 보정하는 방법도 일반적인 해결책이 되지 않습니다. 어느 주문이 몇 번 반복됐는지와 각 주문의 금액이 함께 영향을 줍니다. 원래 집계 단위에서 다시 계산해야 합니다.

GA4의 이벤트 단위와 상품 단위도 구분하기

GA4 전자상거래 문서는 이벤트 전체의 정보를 담는 event scope와 상품 정보를 담는 item scope를 구분합니다. purchase 이벤트의 전체 거래 정보와 items 배열의 각 상품 정보를 같은 단위로 취급하지 않는 이유입니다. GA4 전자상거래 범위

Google은 전자상거래 이벤트 횟수와 상품 수량을 다루는 지표도 구분해 설명합니다. 분석 화면에서는 지표의 포함 이벤트와 지원되는 차원 조합을 함께 확인해야 합니다. 이벤트 단위 지표와 상품 단위 지표가 이름이 비슷하다고 서로 대신할 수는 없습니다. GA4 전자상거래 지표

이 글의 주문·상품 테이블은 자체 원장을 단순화한 합성 모델입니다. 실제 GA4 내보내기 스키마나 플랫폼의 자동 집계 결과를 그대로 구현한 것은 아닙니다.

SQLite에서 직접 재현하는 방법

주문·상품 데이터 모델 예제 ZIP을 풀고 폴더 안에서 실행합니다. Python 3.9 이상과 표준 라이브러리를 사용합니다. 검증 환경의 SQLite 버전은 3.43.2입니다.

python3 analysis.py
python3 -m unittest

schema.sql로 메모리 내 데이터베이스를 만들고 합성 JSON을 넣은 뒤 실제 SQL을 실행합니다. 파일로 데이터베이스를 저장하거나 외부 데이터베이스에 연결하지 않습니다. 출력에는 집계 값뿐 아니라 상품별 결과, 주문·상품 대사표, 실제로 펼쳐진 다섯 행이 함께 포함됩니다.

검증은 중복 주문 키, 중복 상품 행 키, 연결되지 않은 상품 행, 키의 누락, 잘못된 수량·금액을 확인합니다. 주문에 상품이 없는 경우와 상품 금액 합계가 맞지 않는 경우도 검사합니다. 입력 순서를 바꾸어도 같은 결과가 나오는지 확인했습니다.

주문과 상품 테이블을 읽는 매도비

데이터 모델의 첫 설명에 ‘이 테이블의 한 행은 무엇인가’를 적어 보세요. 그 문장이 정해져야 키, 금액, 수량과 조인 결과를 같은 기준으로 검토할 수 있습니다.

공식 문서는 2026년 9월 6일 확인했습니다. Kimball 자료는 집계 단위의 개념을 설명하는 2008년 원문을 참고했습니다.

마케팅 데이터 모델·품질·지표 정의 연재 6편입니다. 이전 글: 광고비·부가세·수수료·환율을 같은 기준으로 맞추기

다음 글: 사용자·기기·세션·주문 식별자를 구분하는 방법

🐹

자주 묻는 질문

데이터의 grain은 기본키와 같은 뜻인가요?

grain은 한 행이 어떤 업무상 사실이나 측정을 나타내는지 설명하는 정의이고, 기본키는 그 행을 식별하는 구현 수단입니다. 키가 유일해도 다른 단위의 금액을 반복해서 합산하면 지표는 잘못될 수 있습니다.

주문과 상품을 조인하면 조인 자체가 잘못된 건가요?

그렇지는 않습니다. 주문의 각 상품을 보고 싶다면 주문과 상품 행을 연결할 수 있습니다. 다만 조인 결과의 한 행은 상품 행에 대응하므로 반복된 주문 총액을 그대로 더하지 않도록 집계 단위를 확인해야 합니다.

SUM DISTINCT로 중복 매출을 제거할 수 있나요?

금액만 DISTINCT 처리하면 같은 금액의 서로 다른 정상 주문도 하나로 줄어듭니다. 예제에서는 100,000원 주문 두 건과 50,000원 주문 한 건이 150,000원으로 잘못 합산됩니다. 거래 식별자와 금액의 단위를 함께 봐야 합니다.

같은 주문에 같은 상품 코드가 두 번 있으면 중복인가요?

원천의 상품 행 정의에 따라 다릅니다. 같은 상품도 단가·조건이 다른 별도 행일 수 있습니다. 예제는 주문 ID와 행 번호를 상품 행의 키로 사용하고, SKU만으로 행을 삭제하지 않습니다.