본문으로 건너뛰기
최종 수정: 에

Unique 모델의 다중 스트림 업데이트

활용 시나리오와 문제점​

데이터 웨어하우스의 와이드 테이블에서는 동일한 테이블의 서로 다른 열이 서로 다른 데이터 소스나 서로 다른 처리 파이프라인에서 오는 경우가 많습니다. 대표적인 사례는 다음과 같습니다.

  • 실시간 스트림: 테이블의 일부 필드(예: 사용자 행동 관련 열)를 지속적으로 업데이트합니다.
  • 오프라인 스트림(또는 온디맨드 스트림): 특정 시점에 테이블의 다른 필드(예: 사용자 프로필이나 태그 관련 열)를 업데이트합니다.

이러한 시나리오에서는 여러 데이터 스트림이 동일한 기본 키 테이블에 동시에 데이터를 쓰면서 두 가지 핵심 문제에 직면합니다.

문제점설명
동시 쓰기에 따른 덮어쓰기 문제데이터 스트림마다 도착 시점이 다릅니다. 단순한 REPLACE 집계만으로는 각 스트림의 데이터가 버전 순서대로 업데이트된다고 보장할 수 없습니다.
높은 데이터 결합 비용모든 열의 데이터가 모일 때까지 기다렸다가 쓰는 것은 사실상 불가능합니다. 대기 시간이 매우 길어질 수 있으며, 업무에서 이를 감당하기 어렵습니다.

Doris Unique 모델은 Sequence 열을 기반으로 하는 버전 제어를 기본적으로 지원하지만, 전역 Sequence 열 하나만 정의할 수 있으므로 서로 다른 데이터 스트림에서 오는 각 열의 업데이트 버전을 독립적으로 제어할 수 없습니다. 이를 해결하기 위해 Doris는 Sequence Mapping 기능을 제공합니다.

해결 방법: Sequence Mapping​

Sequence Mapping은 Doris Unique 모델이 지원하는 다중 스트림 동시 업데이트 메커니즘입니다. 데이터 열마다 서로 다른 Sequence 열을 지정하여 각 데이터 스트림의 버전을 상호 간섭 없이 독립적으로 제어할 수 있습니다.

동작 원리​

Unique 테이블에 다음과 같은 열이 있다고 가정합니다.

열ABCDEs1s2
역할KeyKeyValueValueValueSequenceSequence
  • 데이터 스트림 1은 A, B, C, D 열에 데이터를 쓰고, s1을 버전 제어 열로 사용합니다.
  • 데이터 스트림 2는 A, B, E 열에 데이터를 쓰고, s2를 버전 제어 열로 사용합니다.

이 매핑을 사용하면 다음과 같이 동작합니다.

  • s1은 C, D 열의 버전을 제어합니다. 새 데이터의 s1이 기존 데이터의 s1보다 클 때만 C, D, s1이 업데이트됩니다.
  • s2는 E 열의 버전을 제어합니다. 새 데이터의 s2가 기존 데이터의 s2보다 클 때만 E, s2가 업데이트됩니다.
  • 두 스트림은 서로 간섭하지 않으며, 쿼리에서 모든 열의 최신 데이터를 읽을 수 있습니다.

사용 예제​

1. Sequence Mapping을 지원하는 테이블 생성​

sequence_mapping.<sequence_col> 속성을 PROPERTIES에 지정하여 Sequence 열과 해당 열이 제어하는 Value 열 사이의 매핑을 선언합니다.

다음 예제에서는 c, d 열의 버전을 s1으로 제어하고, e 열의 버전을 s2로 제어하는 Unique 테이블을 생성합니다.

CREATE TABLE `upsert_test` (
`a` bigint(20) NULL COMMENT "",
`b` int(11) NULL COMMENT "",
`c` int(11) NULL COMMENT "",
`d` int(11) NULL COMMENT "",
`e` int(11) NULL COMMENT "",
`s1` int(11) NULL COMMENT "",
`s2` int(11) NULL COMMENT ""
) ENGINE=OLAP
UNIQUE KEY(`a`, `b`)
COMMENT "OLAP"
DISTRIBUTED BY HASH(`a`, `b`) BUCKETS 1
PROPERTIES (
"enable_unique_key_merge_on_write" = "false",
"light_schema_change" = "true",
"replication_num" = "1",
"sequence_mapping.s1" = "c,d",
"sequence_mapping.s2" = "e"
);

Sequence 열에서 지원하는 타입은 정수 타입, DATE, DATETIME입니다. 열을 생성한 후에는 타입을 변경할 수 없습니다.

생성 후 테이블 스키마는 다음과 같습니다.

MySQL > desc upsert_test;
+-------+--------+------+-------+---------+---------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------+------+-------+---------+---------+
| a | bigint | Yes | true | NULL | |
| b | int | Yes | true | NULL | |
| c | int | Yes | false | NULL | REPLACE |
| d | int | Yes | false | NULL | REPLACE |
| e | int | Yes | false | NULL | REPLACE |
| s1 | int | Yes | false | NULL | REPLACE |
| s2 | int | Yes | false | NULL | REPLACE |
+-------+--------+------+-------+---------+---------+

2. 데이터 쓰기 및 쿼리​

다음 다섯 번의 삽입 작업은 서로 다른 Sequence 값이 각 열의 업데이트에 어떤 영향을 미치는지 보여 줍니다.

삽입 1: c, d, s1에만 데이터를 쓰고, e, s2에는 쓰지 않습니다.

MySQL > insert into upsert_test(a, b, c, d, s1) values (1,1,2,2,2);
Query OK, 1 row affected (0.080 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | e | s1 | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 2 | 2 | NULL | 2 | NULL |
+------+------+------+------+------+------+------+

삽입 2: s1=1이 기존 값인 s1=2보다 작으므로 c, d, s1은 업데이트되지 않습니다.

MySQL > insert into upsert_test(a, b, c, d, s1) values (1,1,1,1,1);
Query OK, 1 row affected (0.048 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | e | s1 | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 2 | 2 | NULL | 2 | NULL |
+------+------+------+------+------+------+------+

삽입 3: e, s2에 데이터를 씁니다. 기존 s2가 NULL(가장 작은 값으로 처리됨)이었으므로 e, s2가 업데이트됩니다.

MySQL > insert into upsert_test(a, b, e, s2) values (1,1,2,2);
Query OK, 1 row affected (0.043 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | e | s1 | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 2 | 2 | 2 | 2 | 2 |
+------+------+------+------+------+------+------+

삽입 4: s1=3이 기존 값인 s1=2보다 크므로 c, d, s1이 업데이트되며, e, s2는 영향을 받지 않습니다.

MySQL > insert into upsert_test(a, b, c, d, s1) values (1,1,3,3,3);
Query OK, 1 row affected (0.049 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | e | s1 | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 3 | 3 | 2 | 3 | 2 |
+------+------+------+------+------+------+------+

삽입 5: 두 열 그룹 모두에 데이터를 씁니다. s1=4와 s2=4가 모두 기존 값보다 크므로 모든 열이 업데이트됩니다.

MySQL > insert into upsert_test(a, b, c, d, s1, e, s2) values (1,1,5,5,4,5,4);
Query OK, 1 row affected (0.050 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | e | s1 | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 5 | 5 | 5 | 4 | 4 |
+------+------+------+------+------+------+------+

업데이트 결과를 요약하면 다음과 같습니다.

작업실행 조건업데이트된 열
삽입 1c, d, s1에 최초 쓰기c, d, s1
삽입 2새 s1 < 기존 s1없음
삽입 3e, s2에 최초 쓰기e, s2
삽입 4새 s1 > 기존 s1c, d, s1
삽입 5새 s1과 s2가 모두 기존 값보다 큼c, d, s1, e, s2

3. 열 추가 또는 삭제​

Sequence Mapping 테이블은 ALTER TABLE을 사용하여 열과 매핑을 동적으로 추가하거나 삭제할 수 있습니다.

초기 테이블 생성: s1 -> c,d 매핑 하나만 포함합니다.

CREATE TABLE `upsert_test` (
`a` bigint(20) NULL COMMENT "",
`b` int(11) NULL COMMENT "",
`c` int(11) NULL COMMENT "",
`d` int(11) NULL COMMENT "",
`s1` int(11) NULL COMMENT ""
) ENGINE=OLAP
UNIQUE KEY(`a`, `b`)
COMMENT "OLAP"
DISTRIBUTED BY HASH(`a`, `b`) BUCKETS 1
PROPERTIES (
"enable_unique_key_merge_on_write" = "false",
"light_schema_change" = "true",
"replication_num" = "1",
"sequence_mapping.s1" = "c,d"
);

초기 데이터 쓰기:

MySQL > insert into upsert_test(a, b, c, d, s1) values (1,1,1,1,1),(1,1,3,3,3),(1,1,2,2,2);
Query OK, 3 rows affected (0.101 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+
| a | b | c | d | s1 |
+------+------+------+------+------+
| 1 | 1 | 3 | 3 | 3 |
+------+------+------+------+------+

열과 매핑 추가: ALTER TABLE을 사용하여 e, s2 열을 추가하면서 s2 -> e 매핑을 함께 선언합니다.

MySQL > alter table upsert_test add column (e int(11) NULL, s2 bigint) PROPERTIES('sequence_mapping.s2' = 'e');
Query OK, 0 rows affected (0.011 sec)

MySQL > desc upsert_test;
+-------+--------+------+-------+---------+---------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------+------+-------+---------+---------+
| a | bigint | Yes | true | NULL | |
| b | int | Yes | true | NULL | |
| c | int | Yes | false | NULL | REPLACE |
| d | int | Yes | false | NULL | REPLACE |
| s1 | int | Yes | false | NULL | REPLACE |
| e | int | Yes | false | NULL | REPLACE |
| s2 | bigint | Yes | false | NULL | REPLACE |
+-------+--------+------+-------+---------+---------+

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | s1 | e | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 3 | 3 | 3 | NULL | NULL |
+------+------+------+------+------+------+------+

새 매핑이 적용된 후 데이터 쓰기:

MySQL > insert into upsert_test(a, b, e, s2) values (1,1,2,2);
Query OK, 1 row affected (0.052 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | s1 | e | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 3 | 3 | 3 | 2 | 2 |
+------+------+------+------+------+------+------+

MySQL > insert into upsert_test(a, b, c, d, s1, e, s2) values (1,1,5,5,4,5,4);
Query OK, 1 row affected (0.050 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+------+
| a | b | c | d | s1 | e | s2 |
+------+------+------+------+------+------+------+
| 1 | 1 | 5 | 5 | 4 | 5 | 4 |
+------+------+------+------+------+------+------+

열 삭제: Value 열인 e를 삭제하면 해당 매핑이 무효화됩니다. 그런 다음 Sequence 열인 s2를 삭제합니다.

MySQL > alter table upsert_test drop column e;
Query OK, 0 rows affected (0.006 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+------+
| a | b | c | d | s1 | s2 |
+------+------+------+------+------+------+
| 1 | 1 | 5 | 5 | 4 | 4 |
+------+------+------+------+------+------+

MySQL > alter table upsert_test drop column s2;
Query OK, 0 rows affected (0.005 sec)

MySQL > select * from upsert_test;
+------+------+------+------+------+
| a | b | c | d | s1 |
+------+------+------+------+------+
| 1 | 1 | 5 | 5 | 4 |
+------+------+------+------+------+

사용 제약 사항​

Sequence Mapping을 사용할 때는 다음 제약 사항에 유의해야 합니다.

범주제약 사항
테이블 생성 설정light_schema_change를 활성화해야 합니다. 테이블 생성 시 sequence_mapping 속성을 선언하지 않으면 나중에 활성화할 수 없습니다.
열 타입Sequence 열은 정수 타입과 시간 타입(DATE, DATETIME)만 지원하며, 생성 후에는 타입을 변경할 수 없습니다.
열 역할Sequence 열과 매핑된 열은 모두 Key 열이 될 수 없습니다. 모든 비 Key 열은 Sequence 열에 매핑되어야 합니다.
매핑 관계서로 다른 Sequence 열에 매핑된 열은 겹칠 수 없습니다(예: d를 s1과 s2 모두에 매핑할 수 없음). 매핑을 설정한 후에는 변경할 수 없습니다(예: 이미 s1에 매핑된 열을 s2에 다시 매핑할 수 없음).
DDL 제한 사항열 이름 변경은 지원하지 않습니다. Rollup 생성은 지원하지 않습니다.
저장 모드현재 MOR (Merge-on-Read) 테이블만 지원합니다. 전역 Sequence 열과 함께 활성화하는 것은 지원하지 않습니다. 일괄 삭제 작업은 지원하지 않습니다.
데이터 적재 시 동작데이터 적재 시 포함되지 않은 필드는 기본값 또는 NULL로 자동으로 채워집니다. Sequence 열을 비교할 때 NULL은 가장 작은 값으로 처리됩니다.