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

Unique Key 모델의 동시 업데이트 제어

Unique Key 모델에서 Doris는 포괄적인 동시 업데이트 제어 기능을 제공합니다. 주로 다음 세 가지 일반적인 시나리오를 다룹니다:

사용자 시나리오문제점Doris 해결 방식
동일한 기본 키에 대한 여러 차례의 데이터 적재동시 데이터 적재 시 최종적으로 반영할 레코드를 결정해야 함MVCC(Multi-Version Concurrency Control): 버전 번호에 따라 덮어쓰기 순서 결정
여러 스레드의 동기화로 인한 데이터 도착 순서 역전이전 데이터가 나중에 도착하여 최신 데이터를 잘못 덮어씀Sequence 컬럼: 사용자가 교체 순서를 지정하며, Sequence 값이 더 큰 행이 최종 반영됨
동일한 테이블에 대한 동시 UPDATE행 단위 업데이트 시 더티 데이터가 발생할 수 있어 기본적으로 직렬 실행테이블 수준 잠금으로 Serializable 격리 수준을 보장하며, 동시성 제한을 해제하는 옵션 제공

다음 절에서는 이러한 메커니즘의 동작 원리와 사용 방법을 설명합니다.

MVCC 다중 버전 동시성 제어​

Doris는 Multi-Version Concurrency Control(MVCC)을 사용하여 동시 업데이트를 관리합니다:

  • 각 데이터 쓰기 작업에는 쓰기 트랜잭션이 할당되며, 이 트랜잭션은 쓰기의 원자성을 보장합니다(전체 성공 또는 전체 실패).
  • 쓰기 트랜잭션이 커밋되면 시스템이 버전 번호를 할당합니다.
  • Unique Key 테이블에 데이터를 여러 번 적재할 때 중복된 기본 키가 있으면 Doris는 버전 번호에 따라 덮어쓰기 순서를 결정합니다. 버전 번호가 높은 데이터가 버전 번호가 낮은 데이터를 덮어씁니다.

UPDATE 동시성 제어​

기본 동작​

기본적으로 Doris는 동일한 테이블에서 여러 UPDATE 작업을 동시에 실행하는 것을 허용하지 않습니다.

이는 Doris가 현재 행 단위 업데이트를 지원하기 때문입니다. SET v2 = 1만 선언하더라도 다른 모든 Value 컬럼도 함께 덮어씁니다(값 자체는 바뀌지 않음). 두 UPDATE 작업이 동일한 행을 동시에 업데이트하면 동작이 정의되어 있지 않아 더티 데이터가 발생할 위험이 있습니다.

이 때문에 UPDATE 문은 기본적으로 테이블 수준 잠금을 통해 Serializable 격리를 제공하므로 여러 UPDATE 작업은 직렬로만 실행됩니다.

동시성 제한 해제​

실제 사용 환경에서 동시 업데이트가 동일한 행을 동시에 처리하지 않도록 보장할 수 있다면 FE 설정을 변경하여 동시성 제한을 해제할 수 있습니다:

설정 항목기본값설명
enable_concurrent_updatefalsetrue로 설정하면 UPDATE의 동시 실행을 허용하지만 트랜잭션은 더 이상 보장하지 않음

Sequence 컬럼​

Sequence 컬럼이 필요한 이유​

Unique 모델은 주로 기본 키의 고유성이 필요한 시나리오를 위해 설계되었으며, 기본 키의 고유성을 보장할 수 있습니다. 그러나 다음과 같은 상황에서는 데이터 적재 버전 번호만으로는 충분하지 않습니다:

  • 여러 스레드가 동시에 Doris로 데이터를 동기화하면 서로 다른 스레드의 데이터가 순서대로 도착하지 않을 수 있습니다.
  • 이전 데이터가 나중에 도착하여 최신 데이터를 잘못 덮어쓸 수 있습니다.
  • 동일한 기본 키를 가진 데이터를 같은 배치 또는 서로 다른 배치에서 적재할 때 교체 순서가 보장되지 않으므로 최종 결과가 비결정적입니다.

이 문제를 해결하기 위해 Doris는 데이터 적재 시 Sequence 컬럼을 지정하는 기능을 지원합니다. Key 컬럼이 동일한 행은 Sequence 컬럼 값을 기준으로 교체됩니다. 큰 값이 작은 값을 대체하며, 그 반대의 경우에는 교체되지 않습니다. 이를 통해 사용자가 데이터의 순서를 직접 결정할 수 있습니다.

구현상 Doris는 숨겨진 컬럼 __DORIS_SEQUENCE_COL__을 추가합니다. 컬럼 타입은 테이블 생성 시 사용자가 지정하고, 값은 데이터 적재 시 결정됩니다. Doris는 이 값을 사용하여 Key 컬럼이 동일한 행 중 어떤 행을 최종 반영할지 결정합니다.

참고

현재 Sequence 컬럼은 Unique 모델에서만 지원됩니다.

Sequence 컬럼 활성화​

다음 방법으로 활성화할 수 있습니다:

시나리오작업
새 테이블에서 활성화function_column.sequence_col 또는 function_column.sequence_type을 PROPERTIES에 설정하여 CREATE TABLE 문을 실행합니다
기존 테이블에서 활성화ALTER TABLE example_db.my_table ENABLE FEATURE "SEQUENCE_LOAD" WITH PROPERTIES ("function_column.sequence_type" = "Date")를 실행합니다
활성화 여부 확인SET show_hidden_columns=true를 실행한 다음 desc tablename을 실행합니다. 출력에 __DORIS_SEQUENCE_COL__이 있으면 활성화된 상태입니다

두 속성의 차이는 다음과 같습니다:

속성의미스키마에 해당 컬럼이 필요한지 여부
function_column.sequence_colSequence 컬럼을 테이블의 기존 컬럼에 매핑예
function_column.sequence_typeSequence 컬럼의 타입만 지정하며, 값은 숨겨진 컬럼에 저장아니요

지원되는 컬럼 타입: 정수 타입, DATE, DATETIME, TIMESTAMP_NS. 테이블 생성 후에는 컬럼 타입을 변경할 수 없습니다.

사용 예제​

다음 예제에서는 Stream Load를 사용하여 전체 절차를 설명합니다.

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

Unique 모델 테이블 test_table을 생성하고 Sequence 컬럼을 테이블의 modify_date 컬럼에 매핑합니다:

CREATE TABLE test.test_table
(
user_id bigint,
date date,
group_id bigint,
modify_date date,
keyword VARCHAR(128)
)
UNIQUE KEY(user_id, date, group_id)
DISTRIBUTED BY HASH (user_id) BUCKETS 32
PROPERTIES(
"function_column.sequence_col" = 'modify_date',
"replication_num" = "1",
"in_memory" = "false"
);

생성된 테이블의 스키마는 다음과 같습니다:

MySQL>  desc test_table;
+-------------+--------------+------+-------+---------+---------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-------+---------+---------+
| user_id | BIGINT | No | true | NULL | |
| date | DATE | No | true | NULL | |
| group_id | BIGINT | No | true | NULL | |
| modify_date | DATE | No | false | NULL | REPLACE |
| keyword | VARCHAR(128) | No | false | NULL | REPLACE |
+-------------+--------------+------+-------+---------+---------+

이 컬럼을 테이블 스키마에 표시하지 않으려면 대신 function_column.sequence_type으로 타입을 지정합니다:

PROPERTIES (
"function_column.sequence_type" = 'Date'
);

2. 데이터 적재 및 Sequence 동작 확인​

컬럼 매핑 방식(function_column.sequence_col)을 사용할 때는 데이터 적재 명령에 추가 매개변수가 필요하지 않습니다. Stream Load를 사용하여 다음 데이터를 적재합니다:

1	2020-02-22	1	2020-02-21	a
1 2020-02-22 1 2020-02-22 b
1 2020-02-22 1 2020-03-05 c
1 2020-02-22 1 2020-02-26 d
1 2020-02-22 1 2020-02-23 e
1 2020-02-22 1 2020-02-24 b

Stream Load 명령:

curl --location-trusted -u root: -T testData http://host:port/api/test/test_table/_stream_load

쿼리 결과:

MySQL> select * from test_table;
+---------+------------+----------+-------------+---------+
| user_id | date | group_id | modify_date | keyword |
+---------+------------+----------+-------------+---------+
| 1 | 2020-02-22 | 1 | 2020-03-05 | c |
+---------+------------+----------+-------------+---------+

2020-03-05가 Sequence 컬럼(modify_date)에서 가장 큰 값이므로 keyword 컬럼에는 최종적으로 c가 남습니다.

3. 다양한 데이터 적재 방식에서 Sequence 컬럼 지정​

테이블 생성 시 function_column.sequence_type을 사용하는 경우(즉, Sequence 컬럼이 테이블의 컬럼에 직접 매핑되지 않은 경우), 데이터 적재 시 Sequence 컬럼과 데이터 컬럼 간의 매핑을 명시적으로 지정해야 합니다.

Stream Load

헤더에서 function_column.sequence_col을 사용하여 숨겨진 컬럼에 대한 source_sequence 매핑을 지정합니다:

curl --location-trusted -u root \
-H "columns: k1,k2,source_sequence,v1,v2" \
-H "function_column.sequence_col: source_sequence" \
-T testData http://host:port/api/testDb/testTbl/_stream_load

Broker Load

숨겨진 컬럼에 매핑할 source_sequence 필드를 ORDER BY 절에 지정합니다:

LOAD LABEL db1.label1
(
DATA INFILE("hdfs://host:port/user/data/*/test.txt")
INTO TABLE `tbl1`
COLUMNS TERMINATED BY ","
(k1,k2,source_sequence,v1,v2)
ORDER BY source_sequence
)
WITH BROKER 'broker'
(
"username"="user",
"password"="pass"
)
PROPERTIES
(
"timeout" = "3600"
);

Routine Load

매핑 방식은 위와 동일합니다. 예제:

CREATE ROUTINE LOAD example_db.test1 ON example_tbl 
[WITH MERGE|APPEND|DELETE]
COLUMNS(k1, k2, source_sequence, v1, v2),
WHERE k1 100 and k2 like "%doris%"
[ORDER BY source_sequence]
PROPERTIES
(
"desired_concurrent_number"="3",
"max_batch_interval" = "20",
"max_batch_rows" = "300000",
"max_batch_size" = "209715200",
"strict_mode" = "false"
)
FROM KAFKA
(
"kafka_broker_list" = "broker1:9092,broker2:9092,broker3:9092",
"kafka_topic" = "my_topic",
"kafka_partitions" = "0,1,2,3",
"kafka_offsets" = "101,0,0,200"
);

4. 교체 순서 보장 확인​

앞선 예제에 이어, 모든 Sequence 값이 기존 최댓값 2020-03-05보다 작은 다음 데이터를 적재합니다:

1	2020-02-22	1	2020-02-22	a
1 2020-02-22 1 2020-02-23 b

쿼리 결과:

MySQL [test]> select * from test_table;
+---------+------------+----------+-------------+---------+
| user_id | date | group_id | modify_date | keyword |
+---------+------------+----------+-------------+---------+
| 1 | 2020-02-22 | 1 | 2020-03-05 | c |
+---------+------------+----------+-------------+---------+

이번에 적재한 데이터의 모든 Sequence 값이 기존 최댓값 2020-03-05보다 작으므로 결과는 바뀌지 않습니다.

이제 더 큰 Sequence 값을 가진 데이터를 적재합니다:

1	2020-02-22	1	2020-02-22	a
1 2020-02-22 1 2020-03-23 w

쿼리 결과:

MySQL [test]> select * from test_table;
+---------+------------+----------+-------------+---------+
| user_id | date | group_id | modify_date | keyword |
+---------+------------+----------+-------------+---------+
| 1 | 2020-02-22 | 1 | 2020-03-23 | w |
+---------+------------+----------+-------------+---------+

새로 적재한 데이터의 Sequence 값 2020-03-23이 기존 최댓값 2020-03-05보다 크므로 기존 데이터가 교체됩니다.

결론: 데이터 적재 시 Doris는 모든 배치의 Sequence 컬럼 값을 비교하여 가장 큰 값을 가진 레코드를 테이블에 기록합니다.

사용 시 주의 사항​

  1. Sequence 컬럼을 명시적으로 지정해야 합니다: Stream Load 및 Broker Load와 같은 데이터 적재 작업과 행 업데이트용 INSERT 문에서는 Sequence 컬럼을 명시적으로 지정해야 합니다(기본값이 CURRENT_TIMESTAMP인 경우 제외). 그렇지 않으면 다음 오류가 발생합니다:

    Table test_tbl has sequence column, need to specify the sequence column
  2. Sequence 컬럼 제약 조건 검사 비활성화: 테이블 복제나 내부 데이터 마이그레이션과 같은 시나리오에서는 세션 변수를 통해 INSERT 문에 대한 Sequence 컬럼 필수 지정 검사를 비활성화할 수 있습니다:

    set require_sequence_in_insert = false;
  3. 부분 컬럼 업데이트와의 호환성: 버전 2.0부터 Doris Unique Key 테이블의 Merge-on-Write 구현은 부분 컬럼 업데이트를 지원합니다. 부분 컬럼 업데이트 방식으로 데이터를 적재할 때는 매번 일부 컬럼만 업데이트할 수 있으므로 Sequence 컬럼은 필수가 아닙니다:

    • 데이터 적재 작업에 Sequence 컬럼이 포함되어 있으면 동작은 기존과 동일합니다.
    • 데이터 적재 작업에 Sequence 컬럼이 포함되어 있지 않으면 Doris는 일치하는 기존 데이터의 Sequence 컬럼 값을 업데이트된 행의 Sequence 컬럼 값으로 사용합니다.
    • 기존 데이터에 동일한 Key를 가진 레코드가 없으면 Doris가 자동으로 null 또는 기본값을 채웁니다.
  4. 동시 데이터 적재 시 정확성 보장: 동시 데이터 적재가 발생하면 Doris는 MVCC를 사용하여 데이터의 정확성을 보장합니다. 두 데이터 적재 배치가 동일한 Key의 서로 다른 컬럼을 업데이트하는 경우, 버전 번호가 낮은 데이터 적재 작업이 성공한 후 버전 번호가 높은 데이터 적재 작업은 낮은 버전의 작업이 동일한 Key에 대해 기록한 행을 사용하여 누락된 값을 채웁니다.