사전 중복 적재
문제

중복 적재

gabury1고친 사람 github-actions[bot]

중복 적재는 같은 데이터가 옮겨 넣는 테이블에 두 번 들어가는 고장입니다. 어제 주문 5천 건을 옮긴 테이블에 8천 건이 쌓여 있는 식입니다. 먼저 들어간 3천 건이 한 번 더 들어간 것입니다. 대개 데이터를 옮기다 도중에 멈춘 작업을 처음부터 다시 돌린 것이 원인입니다. 에러 없이 지나가서 합계가 틀린 보고서를 받고서야 드러납니다.

쉽고 빠른 이해

중복 적재는 옮겨 담은 데이터가 두 벌로 쌓이는 고장입니다. 주문 한 건이 매출 표에 두 줄로 들어가면 그날 매출이 부풀어 보입니다.

이 고장에 이름이 따로 붙은 까닭은 조용하기 때문입니다. 두 번째로 넣는 것도 평범한 쓰기라서 에러가 나지 않습니다. 숫자만 틀린 채 다음 단계로 흘러갑니다.

어떻게 생기나:

  1. 적재 작업이 데이터를 몇 묶음으로 나눠 넣다가 중간에 멈춥니다
  2. 앞서 넣은 묶음은 테이블에 남습니다
  3. 작업을 처음부터 다시 돌리면 남아 있던 묶음이 한 번 더 들어갑니다

무엇이 나빠지나 — 합계와 건수가 부풀어 보고서가 틀립니다. 겹친 두 줄이 모든 값에서 같으면 어느 쪽을 지울지 가를 수도 없습니다.

상세

중복 적재는 원천에 한 번 있던 행이 대상 테이블에 두 번 이상 들어간 고장입니다. 넣는 쪽이 이미 들어간 행을 모른 채 같은 범위를 한 번 더 넣을 때 생깁니다.

이 절은 밤마다 주문을 옮기는 작업 하나를 예로 듭니다. 그 작업이 멈췄다가 다시 도는 순서를 따라가며 중복이 생기는 순간을 짚습니다.

적재와 다시 돌리기

적재는 옮겨 온 데이터를 대상 테이블에 넣는 일입니다. 주문 데이터베이스에서 꺼낸 어제 치 주문을 매출 테이블에 넣는 것이 그 예입니다.

데이터를 꺼내 오는 쪽을 원천, 넣는 쪽을 대상이라고 부릅니다. 앞의 예에서 원천은 주문 데이터베이스입니다. 대상은 매출 테이블입니다.

적재는 ETL(Extract, Transform, Load, 추출·변환·적재)의 마지막 단계입니다. ETL 은 흩어진 원천에서 데이터를 꺼내 다듬은 뒤 분석용 저장소에 모으는 작업입니다. 이런 작업은 대개 밤마다 정해진 때에 한 번씩 돕니다.

적재 작업은 도중에 자주 멈춥니다. 데이터베이스 연결이 잠깐 끊기거나 숫자 칸에 글자가 섞여 들어오는 식입니다. 멈춘 작업을 살리는 가장 쉬운 길은 같은 작업을 처음부터 다시 돌리는 것입니다. 중복 적재는 이 다시 돌리기에서 생깁니다.

절반쯤 들어간 뒤 다시 돌린 작업

예의 작업은 덧붙이기로 넣습니다. 덧붙이기는 테이블에 무엇이 이미 있는지 보지 않고 새 행을 더하기만 하는 방식입니다. 아무 조건 없는 INSERT 문이 이렇게 동작합니다.

작업은 어제 치 주문 5천 건을 천 건씩 다섯 묶음으로 나눠 넣습니다. 묶음 하나를 넣을 때마다 커밋합니다. 커밋은 넣은 행을 테이블에 확정하는 일입니다. 확정된 행은 작업이 나중에 실패해도 사라지지 않습니다.

셋째 묶음까지 커밋한 뒤 원천과의 연결이 끊깁니다. 작업은 실패로 끝납니다. 그래도 테이블에는 이미 3천 건이 남아 있습니다. 이렇게 일부만 반영된 채 끝난 상태를 부분 실패라고 부릅니다.

이제 스케줄러가 작업을 처음부터 다시 띄웁니다. 스케줄러는 작업을 정해진 때에 띄우고 실패하면 다시 띄우는 도구입니다. 아래 그림이 두 실행을 이어 보입니다.

sequenceDiagram
    participant 작업
    participant 원천
    participant 테이블 as 매출 테이블
    작업->>원천: 어제 치 5천 건을 읽는다
    작업->>테이블: 묶음 셋을 넣고 커밋한다
    Note over 테이블: 3천 건
    작업-x원천: 연결이 끊겨 실패한다
    Note over 작업: 처음부터 다시 돈다
    작업->>원천: 같은 5천 건을 다시 읽는다
    작업->>테이블: 묶음 다섯을 넣고 커밋한다
    Note over 테이블: 8천 건

다시 돈 작업은 다섯 묶음을 다 넣습니다. 테이블에는 먼저 남은 3천 건 위에 5천 건이 더해져 8천 건이 쌓입니다. 첫 실행이 넣은 3천 건이 두 벌이 된 것입니다.

두 번째 실행은 첫 실행이 무엇을 남겼는지 모른 채 시작했습니다. 넣을 때도 이미 있는 행을 보지 않았습니다. 이 두 가지 모름이 겹친 결과가 중복 적재입니다.

터지는 조건

아래에서 「키」는 행 하나를 가리키는 값입니다. 매출 테이블이면 주문 번호가 키입니다. 같은 주문 번호가 두 줄이면 그 주문이 두 번 들어간 것입니다.

넷이 다 서면 중복 적재가 됩니다. 하나라도 빠지면 겹침이 생기지 않거나 에러로 멈춥니다.

조건 이 조건이 빠지면
1. 앞선 실행이 넣은 행이 대상 테이블에 남아 있다 다시 도는 실행이 빈 곳에 처음 넣는 것과 같다
2. 같은 범위를 넣는 실행이 한 번 더 돈다 남은 행 위에 겹쳐 넣을 일이 없다
3. 넣는 방식이 덧붙이기다 같은 키가 있으면 고치고 없으면 넣는 업서트는 겹치지 않는다
4. 같은 키를 두 번 못 넣게 막는 유니크 제약이 없다 두 번째 삽입이 에러로 멈춘다

넷째 줄이 이 고장을 조용하게 만듭니다. 유니크 제약이 있으면 두 번째 삽입이 에러를 내고 작업이 멈춥니다. 제약이 없으면 두 번째 삽입도 성공으로 끝납니다.

유니크 제약을 안 걸어 둔 테이블에서는 넷째 조건이 늘 서 있습니다. 그러면 나머지 셋만 맞아도 중복이 쌓입니다.

한 번 더 도는 실행이 생기는 경로

둘째 조건은 손으로 다시 돌릴 때만 서는 것이 아닙니다. 같은 범위를 한 번 더 넣게 되는 길은 여럿입니다.

증분 추출은 지난번 이후 바뀐 행만 가져오는 방식입니다. 어디까지 가져왔는지는 기준 시각 하나로 적어 둡니다. 다음 실행은 그 시각 뒤의 행만 읽습니다. 아래 표의 마지막 줄이 이 방식에서 생기는 경로입니다.

경로 무엇이 한 번 더 도나
손으로 다시 돌리기 실패한 작업을 운영자가 처음부터 다시 띄운다
자동 재시도 스케줄러가 실패한 작업을 정해진 횟수만큼 다시 띄운다
겹쳐 뜬 실행 앞 실행이 안 끝났는데 다음 주기의 실행이 같은 범위를 넣는다
겹친 백필 지난 기간을 다시 채우는 작업이 이미 들어간 날짜까지 덮는다
기준 시각을 못 남긴 증분 추출 다음 실행이 지난번과 같은 범위를 다시 읽는다

마지막 줄은 실패한 작업이 없어 보여도 생깁니다. 적재가 끝난 뒤 기준 시각을 적기 전에 작업이 죽었다고 합시다. 테이블에는 행이 들어갔지만 기준 시각은 옛 값에 머뭅니다. 다음 실행은 방금 넣은 범위를 처음 보는 것으로 알고 다시 넣습니다.

에러 없이 숫자만 틀린다

중복 적재는 에러를 남기지 않습니다. 두 번째 삽입도 첫 번째와 똑같이 성공합니다. 작업 기록에는 적재 완료만 남습니다.

틀린 숫자는 다음 단계로 흘러갑니다. 매출 테이블을 더하는 집계가 그날 매출을 부풀려 냅니다. 대개 보고서의 숫자가 다른 부서가 가진 숫자와 안 맞을 때 드러납니다.

가장 쉽게 알아채는 방법은 행 수를 견주는 것입니다. 원천에서 읽은 행 수와 대상에 들어간 행 수를 나란히 셉니다. 원천 테이블은 orders, 대상인 매출 테이블은 dw_orders 입니다. :day 에는 어제 날짜가 들어갑니다.

SQL
SELECT COUNT(*) FROM orders
WHERE order_date = :day;      -- 5000

SELECT COUNT(*) FROM dw_orders
WHERE order_date = :day;      -- 8000

원천은 5천 건, 대상은 8천 건입니다. 차이 3천 건이 두 번 들어간 행입니다.

어느 행이 겹쳤는지는 키로 묶어 셉니다. 같은 주문 번호가 두 번 이상 나오면 겹친 것입니다.

SQL
SELECT order_id, COUNT(*)
FROM dw_orders
WHERE order_date = :day
GROUP BY order_id
HAVING COUNT(*) > 1;   -- 3000행 · 모두 2

결과는 3천 행입니다. 행마다 개수가 2입니다. 첫 실행이 남긴 3천 건이 모두 한 번씩 더 들어갔다는 뜻입니다.

겹친 두 행은 주문 번호까지 모든 열이 같습니다. 이러면 보통의 DELETE 조건으로는 둘을 가를 수 없습니다. 조건에 맞는 행이 둘 다 지워지기 때문입니다. 대개 그날 치를 전부 지우고 다시 넣는 쪽으로 복구합니다.

원천 중복·중복 처리·데이터 누락과의 경계

이름이 비슷한 고장들은 「원천에 몇 번 있고 대상에 몇 번 있나」로 갈립니다.

고장 원천에는 대상에는
중복 적재 한 번 두 번 이상
원천에 이미 있던 중복 두 번 두 번
데이터 누락 한 번 없다

원천에 이미 겹친 행이 있으면 적재 탓이 아닙니다. 적재는 받은 대로 옮겼을 뿐이라 원천과 대상의 행 수가 같게 나옵니다. 그래서 앞 소절의 행 수 비교로 둘을 가를 수 있습니다.

중복 처리는 더 넓은 이름입니다. 한 번만 일어나야 할 일이 두 번 반영되는 고장을 두루 가리킵니다. 결제가 두 번 되는 것도 중복 처리입니다. 중복 적재는 그 가운데 데이터를 테이블에 옮겨 쌓는 작업에서 난 경우입니다.

데이터 누락은 반대편입니다. 실패한 작업을 다시 돌리지 않으면 누락이 남습니다. 다시 돌리면 중복의 여지가 생깁니다. 그래서 적재 작업은 다시 돌려도 안전하게 짜는 쪽으로 풉니다.

다시 돌려도 겹치지 않게 짜는 법

목표는 적재를 멱등하게 만드는 것입니다. 멱등성은 같은 입력으로 여러 번 돌려도 결과가 한 번 돌린 것과 같은 성질입니다. 멱등한 적재는 몇 번을 다시 돌려도 행이 겹치지 않습니다.

아래 표의 첫 줄에는 트랜잭션이 나옵니다. 트랜잭션은 여러 쓰기를 한 덩이로 묶어 전부 반영하거나 전부 취소하는 단위입니다.

표의 오른쪽 칸은 그 수단이 무너뜨리는 조건입니다. 숫자는 앞 「터지는 조건」 표의 번호입니다. 넷 중 하나만 무너져도 중복 적재는 생기지 않습니다.

수단 하는 일 무너뜨리는 조건
지우고 다시 넣기를 한 트랜잭션으로 그날 치를 지운 뒤 다시 넣는다. 둘이 함께 반영되거나 함께 취소된다 1
스테이징 영역을 거쳐 한 번에 옮기기 임시 테이블에 다 넣은 뒤 한 문장으로 대상에 옮긴다 1
업서트로 넣기 같은 키가 있으면 고치고 없으면 넣는다 3
유니크 제약 걸기 같은 키의 두 번째 삽입을 거부한다 4
기준 시각을 적재와 한 트랜잭션에 적기 행과 기준 시각이 함께 반영된다 2 (증분 추출 경로)

지우고 다시 넣기는 앞선 실행이 남긴 행을 먼저 지웁니다. 스테이징 영역은 도중에 실패해도 대상에 행이 남지 않게 합니다. 대상으로 옮기는 일이 한 문장이라 전부 들어가거나 하나도 안 들어가기 때문입니다. 두 방식 모두 조건 1이 서지 않게 합니다.

업서트와 유니크 제약은 남은 행이 있어도 같은 키를 겹쳐 넣지 않습니다. 유니크 제약은 겹침을 막는 대신 다시 돈 작업을 에러로 멈추게 합니다. 조용한 고장을 시끄러운 고장으로 바꾸는 수단입니다. 대개 업서트나 지우고 다시 넣기와 함께 씁니다.

관련 항목

중복 적재가 일어나는 데이터 이동 작업

ETL · ELT · 적재 · 배치 처리 · 데이터 파이프라인 · 변경 데이터 캡처 · 증분 추출 · 백필

중복 적재를 불러들이는 실패와 다시 돌리기

부분 실패 · 재시도 · 타임아웃 · 체크포인트 · 스케줄러 · 워크플로우 오케스트레이션

다시 돌려도 중복 적재가 안 생기게 하는 성질과 장치

멱등성 · 업서트 · 유니크 제약 · 기본 키 · 트랜잭션 · 원자성 · 스테이징 영역 · 파티션 덮어쓰기 · 중복 제거

중복 적재와 같은 파이프라인에서 나는 데이터 장애

중복 처리 · 데이터 누락 · 늦게 도착한 데이터 · 스키마 드리프트 · 배치 창 초과 · 데이터 불일치

중복 적재를 잡아내는 데이터 검사

데이터 품질 · 행 수 대조 · 데이터 검증 · 데이터 관측성

중복 적재가 쌓이는 저장소

데이터 웨어하우스 · 데이터 레이크 · 데이터 마트 · 테이블 · 파티션

중복의 여지를 정하는 전달 약속

최소 한 번 전달 · 최대 한 번 전달 · 정확히 한 번 전달 · 확인 응답

다른 이름: duplicate load · duplicate loading · 이중 적재