컬럼 업데이트
데이터 업데이트 작업에서는 비즈니스 요구 사항에 따라 행의 일부 필드만 수정해야 하는 경우가 많습니다. 예를 들면 다음과 같습니다.
- 빈번한 실시간 필드 업데이트: 사용자 태그 테이블에서 최신 행동 필드를 실시간으로 갱신해야 광고 및 추천 시스템이 실시간 분석과 의사 결정을 수행할 수 있습니다.
- 여러 소스의 데이터를 와이드 테이블로 조인: 여러 소스 테이블의 데이터를 기본 키로 조인하여 하나의 와이드 테이블로 구성하며, 각 소스 테이블은 일부 컬럼만 제공합니다.
- 데이터 수정: 배치 작업으로 일부 레코드의 특정 필드 값을 수정하고 나머지 필드는 그대로 유지합니다.
일반적인 UPDATE 문은 보통 행 전체를 먼저 읽은 다음 행 전체를 다시 써야 합니다. 이러한 "읽기-수정-쓰기" 트랜잭션은 대량 쓰기 환경에서 성능이 떨어져 높은 처리량 요구 사항을 충족하지 못합니다.
Doris가 제공하는 부분 컬럼 업데이트 기능을 사용하면 데이터 적재 시 행 전체를 먼저 읽을 필요 없이 변경할 컬럼만 쓸 수 있어 업데이트 효율이 크게 향상됩니다. 이 문서에서는 Unique Key Model과 Aggregate Key Model에서 부분 컬럼 업데이트를 수행하는 방법을 설명합니다.
기능 개요
| 데이터 모델 | 구현 방식 | 쓰기 성능 | 쿼리 성능 | 사용 사례 |
|---|---|---|---|---|
| Unique Key (Merge-on-Write) | 쓰기 시 행 전체를 채움 | 중간(IO의 영향을 받음) | 높음 | 실시간 업데이트, 쿼리 성능에 민감한 시나리오 |
Aggregate Key (REPLACE_IF_NOT_NULL) | 쿼리 시 집계 | 높음(일반 데이터 적재와 비슷함) | 낮음(집계 쿼리가 MoW보다 5~10배 느림) | 쓰기 처리량에 민감하며 낮은 쿼리 성능을 허용할 수 있는 시나리오 |
Unique Key Model의 컬럼 업데이트
Unique Key Model에서 데이터를 적재할 때 Doris는 행 전체를 먼저 읽지 않고도 일부 컬럼 데이터를 직접 삽입하거나 업데이트할 수 있어 업데이트 효율이 크게 향상됩니다.
- 버전 2.0에서는 Unique Key의 Merge-on-Write 구현에서만 부분 컬럼 업데이트를 지원합니다.
- 버전 2.0.2부터는
INSERT INTO를 사용하여 부분 컬럼 업데이트를 수행할 수 있습니다. - 동기식 구체화된 뷰가 있는 테이블에서는 부분 컬럼 업데이트를 지원하지 않습니다.
- Schema Change가 진행 중인 테이블에서는 부분 컬럼 업데이트를 지원하지 않습니다.
예제
Doris에 주문 테이블 order_tbl이 있고, 주문 ID가 Key 컬럼이며 주문 상태와 주문 금액이 Value 컬럼이라고 가정합니다. Merge-on-Write Unique-Key 모델로 테이블을 생성하고 초기 행을 적재합니다.
CREATE TABLE order_tbl (
order_id INT,
order_status VARCHAR(64),
order_amount INT
)
UNIQUE KEY (order_id)
DISTRIBUTED BY HASH (order_id) BUCKETS 1
PROPERTIES (
"replication_num" = "1",
"enable_unique_key_merge_on_write" = "true"
);
INSERT INTO order_tbl VALUES (1, 'Pending payment', 100);
현재 데이터는 다음과 같습니다.
| 주문 ID | 주문 금액 | 주문 상태 |
|---|---|---|
| 1 | 100 | 결제 대기 |
+----------+--------------+------------------+
| order_id | order_amount | order_status |
+----------+--------------+------------------+
| 1 | 100 | Pending payment |
+----------+--------------+------------------+
1 row in set (0.01 sec)
사용자가 결제 버튼을 클릭하면 주문 ID가 1인 주문의 상태를 Pending shipment로 변경해야 하며, 주문 금액 필드에는 영향을 주지 않아야 합니다.
데이터 적재를 통한 부분 컬럼 업데이트
Stream Load / Broker Load / Routine Load
다음 CSV 파일을 준비합니다.
1,Pending shipment
데이터 적재 시 다음 헤더를 추가합니다.
partial_columns:true
columns 매개변수에 적재할 컬럼을 지정합니다(모든 Key 컬럼을 포함해야 하며, 그렇지 않으면 업데이트를 수행할 수 없습니다). 다음은 Stream Load 예제입니다.
curl --location-trusted -u root: \
-H "partial_columns:true" \
-H "column_separator:," \
-H "columns:order_id,order_status" \
-T /tmp/update.csv \
http://127.0.0.1:8030/api/db1/order_tbl/_stream_load
INSERT INTO
모든 데이터 모델에서 일부 컬럼만 지정한 INSERT INTO의 기본 동작은 행 전체를 쓰는 것입니다. 잘못된 사용을 방지하기 위해 Merge-on-Write 구현에서도 INSERT INTO는 기본적으로 행 전체에 대한 UPSERT 의미 체계를 따릅니다. 부분 컬럼 업데이트 의미 체계를 활성화하려면 먼저 다음 세션 변수를 설정합니다.
SET enable_unique_key_partial_update=true;
INSERT INTO order_tbl (order_id, order_status) VALUES (1, 'Pending shipment');
Flink Connector
Flink Connector를 사용할 때는 다음 설정을 추가합니다.
'sink.properties.partial_columns' = 'true'
sink.properties.column에 적재할 컬럼을 지정합니다(모든 Key 컬럼을 포함해야 하며, 그렇지 않으면 업데이트를 수행할 수 없습니다).
업데이트 결과
업데이트 후 결과는 다음과 같습니다.
+----------+--------------+-------------------+
| order_id | order_amount | order_status |
+----------+--------------+-------------------+
| 1 | 100 | Pending shipment |
+----------+--------------+-------------------+
1 row in set (0.01 sec)
참고 사항
Merge-on-Write 구현은 최적의 쿼리 성능을 보장하기 위해 쓰기 시 행 전체를 채워야 합니다. 따라서 Merge-on-Write를 사용하는 부분 컬럼 업데이트는 데이터 적재 성능을 어느 정도 떨어뜨립니다.
쓰기 성능 최적화 권장 사항:
-
고성능 SSD 사용: NVMe SSD 또는 고속 클라우드 SSD를 권장합니다. 행을 채울 때 대량의 기존 데이터를 읽으므로 높은 읽기 IOPS와 읽기 처리량이 발생합니다.
-
행 저장 활성화: 행을 채울 때 발생하는 IOPS를 크게 줄이고 데이터 적재 성능을 눈에 띄게 향상할 수 있습니다. 테이블 생성 시 다음 속성으로 활성화합니다.
"store_row_column" = "true"
배치 제약 사항: 동일한 배치 쓰기 작업(데이터 적재 작업 또는 INSERT INTO)의 모든 행은 동일한 컬럼 집합만 업데이트할 수 있습니다. 서로 다른 컬럼을 업데이트하려면 데이터를 여러 배치로 나누어야 합니다. 아래에서 설명하는 유연한 컬럼 업데이트 기능을 사용하면 이 제약을 해소할 수 있습니다.
유연한 컬럼 업데이트
기존 Doris의 부분 컬럼 업데이트 기능에서는 한 번의 데이터 적재에 포함된 모든 행이 동일한 컬럼 집합을 업데이트해야 했습니다. 버전 3.1.0부터 Doris는 더 유연한 업데이트 방식을 지원합니다. 한 번의 데이터 적재에서 각 행이 서로 다른 컬럼 집합을 업데이트할 수 있습니다.
- 유연한 컬럼 업데이트는 Stream Load, Routine Load 및 Stream Load를 기반 데이터 적재 방식으로 사용하는 모든 도구(예: Doris Flink Connector)를 지원합니다.
- 유연한 컬럼 업데이트를 사용할 때는 데이터 적재 파일이 JSON 형식이어야 합니다.
사용 사례
CDC를 사용하여 다른 시스템의 데이터를 Doris로 실시간 동기화할 때, 소스 시스템이 내보내는 레코드는 전체 행이 아니라 기본 키와 업데이트된 컬럼만 포함할 수 있습니다. 이 경우 일정 시간 구간에 모인 배치의 각 행이 서로 다른 컬럼을 업데이트할 수 있으며, 유연한 컬럼 업데이트를 사용하여 이러한 데이터를 Doris에 적재할 수 있습니다.
사용 방법
1. 유연한 컬럼 업데이트 활성화
새 테이블: 테이블 생성 시 다음 속성을 지정하여 Merge-on-Write를 활성화하고 유연한 컬럼 업데이트에 필요한 bitmap 숨김 컬럼을 추가합니다.
"enable_unique_key_merge_on_write" = "true"
"enable_unique_key_skip_bitmap_column" = "true"
기존 테이블: 이전 Doris 버전에서 생성한 기존 Merge-on-Write 테이블은 Doris 업그레이드 후 다음 문으로 유연한 컬럼 업데이트를 활성화할 수 있습니다.
ALTER TABLE db1.tbl1 ENABLE FEATURE "UPDATE_FLEXIBLE_COLUMNS";
실행 후 SHOW CREATE TABLE db1.tbl1을 실행합니다. 결과에 "enable_unique_key_skip_bitmap_column" = "true"가 포함되어 있으면 기능이 정상적으로 활성화된 것입니다.
기존 테이블에 이 방법을 사용하기 전에 대상 테이블에서 light-schema-change가 활성화되어 있는지 확인하세요.
2. 데이터 적재 작업에서 활성화
각 데이터 적재 방식의 설정은 다음과 같습니다.
| 데이터 적재 방식 | 설정 항목 | 값 |
|---|---|---|
| Stream Load | 헤더 | unique_key_update_mode:UPDATE_FLEXIBLE_COLUMNS |
| Flink Doris Connector | sink 속성 | 'sink.properties.unique_key_update_mode' = 'UPDATE_FLEXIBLE_COLUMNS' |
| Routine Load | PROPERTIES | "unique_key_update_mode" = "UPDATE_FLEXIBLE_COLUMNS" |
전체 Routine Load 예제:
CREATE ROUTINE LOAD db1.job1 ON tbl1
PROPERTIES (
"format" = "json",
"unique_key_update_mode" = "UPDATE_FLEXIBLE_COLUMNS"
)
FROM KAFKA (
"kafka_broker_list" = "localhost:9092",
"kafka_topic" = "my_topic",
"property.kafka_default_offsets" = "OFFSET_BEGINNING"
);
ALTER ROUTINE LOAD를 사용하여 기존 Routine Load 작업의 업데이트 모드를 변경할 수도 있습니다.
-- 1. Pause the job
PAUSE ROUTINE LOAD FOR db1.job1;
-- 2. Change the update mode
ALTER ROUTINE LOAD FOR db1.job1
PROPERTIES (
"unique_key_update_mode" = "UPDATE_FLEXIBLE_COLUMNS"
);
-- 3. Resume the job
RESUME ROUTINE LOAD FOR db1.job1;
Routine Load에서 UPDATE_FLEXIBLE_COLUMNS 모드를 사용할 때는 다음 제한 사항이 적용됩니다.
- 데이터 형식은 JSON이어야 합니다(
"format" = "json"). jsonpaths속성을 지정할 수 없습니다.fuzzy_parse옵션을 활성화할 수 없습니다.COLUMNS절을 사용할 수 없습니다.WHERE절을 사용할 수 없습니다.
전체 예제
1단계: 테스트 테이블 생성
CREATE TABLE t1 (
`k` INT NULL,
`v1` BIGINT NULL,
`v2` BIGINT NULL DEFAULT "9876",
`v3` BIGINT NOT NULL,
`v4` BIGINT NOT NULL DEFAULT "1234",
`v5` BIGINT NULL
) UNIQUE KEY(`k`)
DISTRIBUTED BY HASH(`k`) BUCKETS 1
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true",
"enable_unique_key_skip_bitmap_column" = "true"
);
2단계: 테이블의 기존 데이터
MySQL root@127.1:d1> select * from t1;
+---+----+----+----+----+----+
| k | v1 | v2 | v3 | v4 | v5 |
+---+----+----+----+----+----+
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 2 | 2 | 2 |
| 3 | 3 | 3 | 3 | 3 | 3 |
| 4 | 4 | 4 | 4 | 4 | 4 |
| 5 | 5 | 5 | 5 | 5 | 5 |
+---+----+----+----+----+----+
3단계: 유연한 컬럼 업데이트 데이터 준비
각 행은 서로 다른 컬럼 집합을 업데이트할 수 있습니다.
{"k": 0, "__DORIS_DELETE_SIGN__": 1}
{"k": 1, "v1": 10}
{"k": 2, "v2": 20, "v5": 25}
{"k": 3, "v3": 30}
{"k": 4, "v4": 20, "v1": 43, "v3": 99}
{"k": 5, "v5": null}
{"k": 6, "v1": 999, "v3": 777}
{"k": 2, "v4": 222}
{"k": 1, "v2": 111, "v3": 111}
4단계: Stream Load를 통한 데이터 적재
curl --location-trusted -u root: \
-H "Expect:100-continue" \
-H "strict_mode:false" \
-H "format:json" \
-H "read_json_by_line:true" \
-H "unique_key_update_mode:UPDATE_FLEXIBLE_COLUMNS" \
-T test1.json \
-XPUT http://<host>:<http_port>/api/d1/t1/_stream_load
5단계: 업데이트 결과 확인
MySQL root@127.1:d1> select * from t1;
+---+-----+------+-----+------+--------+
| k | v1 | v2 | v3 | v4 | v5 |
+---+-----+------+-----+------+--------+
| 1 | 10 | 111 | 111 | 1 | 1 |
| 2 | 2 | 20 | 2 | 222 | 25 |
| 3 | 3 | 3 | 30 | 3 | 3 |
| 4 | 43 | 4 | 99 | 20 | 4 |
| 5 | 5 | 5 | 5 | 5 | <null> |
| 6 | 999 | 9876 | 777 | 1234 | <null> |
+---+-----+------+-----+------+--------+
제한 사항 및 참고 사항
- 일반적인 부분 컬럼 업데이트와 마찬가지로 유연한 컬럼 업데이트에서도 모든 행에 모든 Key 컬럼이 포함되어야 합니다. 이 요구 사항을 충족하지 않는 행은 필터링되며
filter rows에 집계됩니다.filtered rows가 데이터 적재의max_filter_ratio에서 허용하는 상한을 초과하면 전체 데이터 적재가 실패합니다. 필터링된 각 행은 오류 로그에 기록됩니다. - JSON 객체의 키-값 쌍 중 Key가 대상 테이블의 컬럼 이름과 일치하는 쌍만 적용됩니다. 나머지는 무시됩니다. 또한 Key가
__DORIS_VERSION_COL__,__DORIS_ROW_STORE_COL__또는__DORIS_SKIP_BITMAP_COL__인 키-값 쌍도 무시됩니다. - Variant 컬럼이 포함된 테이블에서는 유연한 컬럼 업데이트를 지원하지 않습니다.
- 동기식 구체화된 뷰가 있는 테이블에서는 유연한 컬럼 업데이트를 지원하지 않습니다.
- 유연한 컬럼 업데이트를 사용할 때는 다음 데이터 적재 매개변수를 지정하거나 활성화할 수 없습니다.
merge_typedeletefuzzy_parsecolumnsjsonpathshidden_columnsfunction_column.sequence_colsqlmemtable_on_sink_nodemove-forwardgroup_commitwhere
새로 삽입되는 행의 동작 제어
세션 변수 또는 데이터 적재 속성인 partial_update_new_key_behavior는 부분 컬럼 업데이트와 유연한 컬럼 업데이트에서 새로 삽입되는 행의 동작을 제어합니다.
| 값 | 동작 |
|---|---|
ERROR | 모든 행의 Key가 테이블에 이미 존재해야 하며, 그렇지 않으면 데이터 적재가 실패합니다. |
APPEND | 기존 행 업데이트와 Key가 존재하지 않는 새 행 삽입을 모두 허용합니다. |
예제 테이블 스키마
CREATE TABLE user_profile (
id INT,
name VARCHAR(10),
age INT,
city VARCHAR(10),
balance DECIMAL(9, 0),
last_access_time DATETIME
) ENGINE=OLAP
UNIQUE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"enable_unique_key_merge_on_write" = "true"
);
테이블의 기존 데이터:
mysql> select * from user_profile;
+----+-------+-----+----------+---------+---------------------+
| id | name | age | city | balance | last_access_time |
+----+-------+-----+----------+---------+---------------------+
| 1 | kevin | 18 | shenzhen | 400 | 2023-07-01 12:00:00 |
+----+-------+-----+----------+---------+---------------------+
시나리오 1: ERROR 모드(새 행 거부)
두 번째와 세 번째 행의 Key(3 및 18)가 원래 테이블에 존재하지 않으므로 다음 삽입은 실패합니다.
SET enable_unique_key_partial_update=true;
SET partial_update_new_key_behavior=ERROR;
INSERT INTO user_profile (id, balance, last_access_time) VALUES
(1, 500, '2023-07-03 12:00:01'),
(3, 23, '2023-07-03 12:00:02'),
(18, 9999999, '2023-07-03 12:00:03');
-- Error:
-- (1105, "errCode = 2, detailMessage = (127.0.0.1)[INTERNAL_ERROR]tablet error:
-- [E-7003]Can't append new rows in partial update when partial_update_new_key_behavior is ERROR.
-- Row with key=[3] is not in table., host: 127.0.0.1")
시나리오 2: APPEND 모드(새 행 허용)
SET enable_unique_key_partial_update=true;
SET partial_update_new_key_behavior=APPEND;
INSERT INTO user_profile (id, balance, last_access_time) VALUES
(1, 500, '2023-07-03 12:00:01'),
(3, 23, '2023-07-03 12:00:02'),
(18, 9999999, '2023-07-03 12:00:03');
실행 후 기존 행이 업데이트되고 새 행 두 개가 추가됩니다. 사용자가 지정하지 않은 컬럼에는 다음 규칙이 적용됩니다.
- 컬럼에 기본값이 있으면 기본값을 사용합니다.
- 그렇지 않고 컬럼이 NULL을 허용하면 NULL을 사용합니다.
- 그 외의 경우 삽입이 실패합니다.
쿼리 결과:
mysql> select * from user_profile;
+----+-------+------+----------+---------+---------------------+
| id | name | age | city | balance | last_access_time |
+----+-------+------+----------+---------+---------------------+
| 1 | kevin | 18 | shenzhen | 500 | 2023-07-03 12:00:01 |
| 3 | NULL | NULL | NULL | 23 | 2023-07-03 12:00:02 |
| 18 | NULL | NULL | NULL | 9999999 | 2023-07-03 12:00:03 |
+----+-------+------+----------+---------+---------------------+
Aggregate Key Model의 컬럼 업데이트
Aggregate 테이블은 주로 사전 집계 시나리오에 사용되지만, 집계 함수를 REPLACE_IF_NOT_NULL로 설정하면 부분 컬럼 업데이트 효과도 얻을 수 있습니다.
테이블 생성
컬럼 업데이트가 필요한 각 필드의 집계 함수를 REPLACE_IF_NOT_NULL로 설정합니다.
CREATE TABLE order_tbl (
order_id INT(11) NULL,
order_amount INT(11) REPLACE_IF_NOT_NULL NULL,
order_status VARCHAR(100) REPLACE_IF_NOT_NULL NULL
) ENGINE=OLAP
AGGREGATE KEY(order_id)
COMMENT 'OLAP'
DISTRIBUTED BY HASH(order_id) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
데이터 쓰기
Stream Load, Broker Load, Routine Load 또는 INSERT INTO 중 어떤 방식을 사용하든 업데이트할 필드의 데이터만 쓰면 됩니다. 추가 매개변수는 필요하지 않습니다.
예제
예제는 앞서 설명한 것과 동일합니다. 이에 해당하는 Stream Load 명령은 다음과 같습니다(추가 헤더는 필요하지 않습니다).
$ cat update.csv
1,Pending shipment
curl --location-trusted -u root: \
-H "column_separator:," \
-H "columns:order_id,order_status" \
-T ./update.csv \
http://127.0.0.1:8030/api/db1/order_tbl/_stream_load
이에 해당하는 INSERT INTO 문은 다음과 같습니다(추가 세션 변수는 필요하지 않습니다).
INSERT INTO order_tbl (order_id, order_status) VALUES (1, 'Pending shipment');
참고 사항
- 쓰기 성능: Aggregate Key Model은 쓰기 중 추가 처리를 수행하지 않으므로 쓰기 성능이 일반 데이터 적재와 동일합니다.
- 쿼리 성능: 쿼리 시 집계는 비용이 많이 듭니다. Unique Key Model의 Merge-on-Write 구현과 비교하면 일반적인 집계 쿼리가 5~10배 느립니다.
- NULL 값 제한:
REPLACE_IF_NOT_NULL집계 함수는 NULL이 아닌 값에만 적용되므로 필드 값을 NULL로 변경할 수 없습니다.