Dynamic 데이터 타입
Dynamic 데이터 타입
모든 타입의 값을 미리 알지 못해도 그 안에 저장할 수 있는 타입이에요. Dynamic(max_types=N)으로 선언하며, 별도로 저장되는 각 데이터 블록(예: MergeTree 테이블의 각 데이터 파트) 안에서 서로 다른 타입을 서브컬럼으로 얼마나 많이 저장할 수 있는지를 제한할 수 있어요.
출처: 문서
본문
이 타입은 모든 타입의 값을 미리 전부 알지 못해도 그 안에 저장할 수 있게 해줘요. Dynamic 타입 컬럼을 선언하려면 다음 문법을 사용해요.
<column_name> Dynamic(max_types=N)
여기서 N은 0과 254 사이의 옵션 매개변수로, 별도로 저장되는 단일 데이터 블록(예: MergeTree 테이블의 단일 데이터 파트) 안의 Dynamic 타입 컬럼에서 서로 다른 타입을 별도의 서브컬럼으로 얼마나 많이 저장할 수 있는지를 나타내요. 이 제한을 초과하면 새 타입을 가진 모든 값이 이진 형식의 특별한 공유 데이터 구조에 함께 저장돼요. max_types의 기본값은 32예요.
Dynamic 만들기 (Creating Dynamic)
테이블 컬럼 정의에 Dynamic 타입 사용하기:
CREATE TABLE test (d Dynamic) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('Hello, World!'), ([1, 2, 3]);
SELECT d, dynamicType(d) FROM test;
┌─d─────────────┬─dynamicType(d)─┐
│ ᴺᵁᴸᴸ │ None │
│ 42 │ Int64 │
│ Hello, World! │ String │
│ [1,2,3] │ Array(Int64) │
└───────────────┴────────────────┘
일반 컬럼에서의 CAST 사용:
SELECT 'Hello, World!'::Dynamic AS d, dynamicType(d);
┌─d─────────────┬─dynamicType(d)─┐
│ Hello, World! │ String │
└───────────────┴────────────────┘
Variant 컬럼에서의 CAST 사용:
SET use_variant_as_common_type = 1;
SELECT multiIf((number % 3) = 0, number, (number % 3) = 1, range(number + 1), NULL)::Dynamic AS d, dynamicType(d) FROM numbers(3)
┌─d─────┬─dynamicType(d)─┐
│ 0 │ UInt64 │
│ [0,1] │ Array(UInt64) │
│ ᴺᵁᴸᴸ │ None │
└───────┴────────────────┘
Dynamic 중첩 타입을 서브컬럼으로 읽기 (Reading Dynamic nested types as subcolumns)
Dynamic 타입은 Dynamic 컬럼에서 타입 이름을 서브컬럼으로 사용해 단일 중첩 타입을 읽는 것을 지원해요. 그래서 d Dynamic 컬럼이 있다면 d.T 문법으로 유효한 타입 T의 서브컬럼을 읽을 수 있어요. 이 서브컬럼은 T가 Nullable 안에 들어갈 수 있으면 Nullable(T), 그렇지 않으면 T 타입이 돼요. 이 서브컬럼은 원래 Dynamic 컬럼과 같은 크기이며, 원래 Dynamic 컬럼이 타입 T를 가지지 않는 모든 행에서 NULL 값(또는 T가 Nullable 안에 들어갈 수 없으면 빈 값)을 담아요. Dynamic 서브컬럼은 함수 dynamicElement(dynamic_column, type_name)으로도 읽을 수 있어요.
예시:
CREATE TABLE test (d Dynamic) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('Hello, World!'), ([1, 2, 3]);
SELECT d, dynamicType(d), d.String, d.Int64, d.`Array(Int64)`, d.Date, d.`Array(String)` FROM test;
┌─d─────────────┬─dynamicType(d)─┬─d.String──────┬─d.Int64─┬─d.Array(Int64)─┬─d.Date─┬─d.Array(String)─┐
│ ᴺᵁᴸᴸ │ None │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │ ᴺᵁᴸᴸ │ [] │
│ 42 │ Int64 │ ᴺᵁᴸᴸ │ 42 │ [] │ ᴺᵁᴸᴸ │ [] │
│ Hello, World! │ String │ Hello, World! │ ᴺᵁᴸᴸ │ [] │ ᴺᵁᴸᴸ │ [] │
│ [1,2,3] │ Array(Int64) │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [1,2,3] │ ᴺᵁᴸᴸ │ [] │
└───────────────┴────────────────┴───────────────┴─────────┴────────────────┴────────┴─────────────────┘
SELECT toTypeName(d.String), toTypeName(d.Int64), toTypeName(d.`Array(Int64)`), toTypeName(d.Date), toTypeName(d.`Array(String)`) FROM test LIMIT 1;
┌─toTypeName(d.String)─┬─toTypeName(d.Int64)─┬─toTypeName(d.Array(Int64))─┬─toTypeName(d.Date)─┬─toTypeName(d.Array(String))─┐
│ Nullable(String) │ Nullable(Int64) │ Array(Int64) │ Nullable(Date) │ Array(String) │
└──────────────────────┴─────────────────────┴────────────────────────────┴────────────────────┴─────────────────────────────┘
SELECT d, dynamicType(d), dynamicElement(d, 'String'), dynamicElement(d, 'Int64'), dynamicElement(d, 'Array(Int64)'), dynamicElement(d, 'Date'), dynamicElement(d, 'Array(String)') FROM test;
┌─d─────────────┬─dynamicType(d)─┬─dynamicElement(d, 'String')─┬─dynamicElement(d, 'Int64')─┬─dynamicElement(d, 'Array(Int64)')─┬─dynamicElement(d, 'Date')─┬─dynamicElement(d, 'Array(String)')─┐
│ ᴺᵁᴸᴸ │ None │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │ ᴺᵁᴸᴸ │ [] │
│ 42 │ Int64 │ ᴺᵁᴸᴸ │ 42 │ [] │ ᴺᵁᴸᴸ │ [] │
│ Hello, World! │ String │ Hello, World! │ ᴺᵁᴸᴸ │ [] │ ᴺᵁᴸᴸ │ [] │
│ [1,2,3] │ Array(Int64) │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [1,2,3] │ ᴺᵁᴸᴸ │ [] │
└───────────────┴────────────────┴─────────────────────────────┴────────────────────────────┴───────────────────────────────────┴───────────────────────────┴────────────────────────────────────┘
각 행에 어떤 변형(variant)이 저장되어 있는지 알려면 함수 dynamicType(dynamic_column)을 사용할 수 있어요. 각 행에 대해 값 타입 이름의 String을 반환해요(행이 NULL이면 'None').
예시:
CREATE TABLE test (d Dynamic) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('Hello, World!'), ([1, 2, 3]);
SELECT dynamicType(d) FROM test;
┌─dynamicType(d)─┐
│ None │
│ Int64 │
│ String │
│ Array(Int64) │
└────────────────┘
Dynamic 컬럼과 다른 컬럼 사이의 변환 (Conversion between Dynamic column and other columns)
Dynamic 컬럼으로 수행할 수 있는 변환은 4가지가 있어요.
일반 컬럼을 Dynamic 컬럼으로 변환 (Converting an ordinary column to a Dynamic column)
SELECT 'Hello, World!'::Dynamic AS d, dynamicType(d);
┌─d─────────────┬─dynamicType(d)─┐
│ Hello, World! │ String │
└───────────────┴────────────────┘
String 컬럼을 파싱을 통해 Dynamic 컬럼으로 변환 (Converting a String column to a Dynamic column through parsing)
String 컬럼에서 Dynamic 타입 값을 파싱하려면 설정 cast_string_to_dynamic_use_inference를 켤 수 있어요.
SET cast_string_to_dynamic_use_inference = 1;
SELECT CAST(materialize(map('key1', '42', 'key2', 'true', 'key3', '2020-01-01')), 'Map(String, Dynamic)') as map_of_dynamic, mapApply((k, v) -> (k, dynamicType(v)), map_of_dynamic) as map_of_dynamic_types;
┌─map_of_dynamic──────────────────────────────┬─map_of_dynamic_types─────────────────────────┐
│ {'key1':42,'key2':true,'key3':'2020-01-01'} │ {'key1':'Int64','key2':'Bool','key3':'Date'} │
└─────────────────────────────────────────────┴──────────────────────────────────────────────┘
Dynamic 컬럼을 일반 컬럼으로 변환 (Converting a Dynamic column to an ordinary column)
Dynamic 컬럼을 일반 컬럼으로 변환할 수 있어요. 이 경우 모든 중첩 타입이 대상 타입으로 변환돼요.
CREATE TABLE test (d Dynamic) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('42.42'), (true), ('e10');
SELECT d::Nullable(Float64) FROM test;
┌─CAST(d, 'Nullable(Float64)')─┐
│ ᴺᵁᴸᴸ │
│ 42 │
│ 42.42 │
│ 1 │
│ 0 │
└──────────────────────────────┘
Variant 컬럼을 Dynamic 컬럼으로 변환 (Converting a Variant column to Dynamic column)
CREATE TABLE test (v Variant(UInt64, String, Array(UInt64))) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('String'), ([1, 2, 3]);
SELECT v::Dynamic AS d, dynamicType(d) FROM test;
┌─d───────┬─dynamicType(d)─┐
│ ᴺᵁᴸᴸ │ None │
│ 42 │ UInt64 │
│ String │ String │
│ [1,2,3] │ Array(UInt64) │
└─────────┴────────────────┘
Dynamic(max_types=N) 컬럼을 다른 Dynamic(max_types=K) 컬럼으로 변환 (Converting a Dynamic(max_types=N) column to another Dynamic(max_types=K))
K >= N이면 변환 중에 데이터가 바뀌지 않아요.
CREATE TABLE test (d Dynamic(max_types=3)) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), (43), ('42.42'), (true);
SELECT d::Dynamic(max_types=5) as d2, dynamicType(d2) FROM test;
┌─d─────┬─dynamicType(d)─┐
│ ᴺᵁᴸᴸ │ None │
│ 42 │ Int64 │
│ 43 │ Int64 │
│ 42.42 │ String │
│ true │ Bool │
└───────┴────────────────┘
K < N이면 가장 희귀한 타입의 값들이 단일 특수 서브컬럼에 들어가지만 여전히 접근할 수 있어요.
CREATE TABLE test (d Dynamic(max_types=4)) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), (43), ('42.42'), (true), ([1, 2, 3]);
SELECT d, dynamicType(d), d::Dynamic(max_types=2) as d2, dynamicType(d2), isDynamicElementInSharedData(d2) FROM test;
┌─d───────┬─dynamicType(d)─┬─d2──────┬─dynamicType(d2)─┬─isDynamicElementInSharedData(d2)─┐
│ ᴺᵁᴸᴸ │ None │ ᴺᵁᴸᴸ │ None │ false │
│ 42 │ Int64 │ 42 │ Int64 │ false │
│ 43 │ Int64 │ 43 │ Int64 │ false │
│ 42.42 │ String │ 42.42 │ String │ false │
│ true │ Bool │ true │ Bool │ true │
│ [1,2,3] │ Array(Int64) │ [1,2,3] │ Array(Int64) │ true │
└─────────┴────────────────┴─────────┴─────────────────┴──────────────────────────────────┘
함수 isDynamicElementInSharedData는 Dynamic 안의 특별한 공유 데이터 구조에 저장된 행에 대해 true를 반환해요. 보시다시피 결과 컬럼에는 공유 데이터 구조에 저장되지 않은 타입이 2개뿐이에요.
K=0이면 모든 타입이 단일 특수 서브컬럼에 들어가요.
CREATE TABLE test (d Dynamic(max_types=4)) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), (43), ('42.42'), (true), ([1, 2, 3]);
SELECT d, dynamicType(d), d::Dynamic(max_types=0) as d2, dynamicType(d2), isDynamicElementInSharedData(d2) FROM test;
┌─d───────┬─dynamicType(d)─┬─d2──────┬─dynamicType(d2)─┬─isDynamicElementInSharedData(d2)─┐
│ ᴺᵁᴸᴸ │ None │ ᴺᵁᴸᴸ │ None │ false │
│ 42 │ Int64 │ 42 │ Int64 │ true │
│ 43 │ Int64 │ 43 │ Int64 │ true │
│ 42.42 │ String │ 42.42 │ String │ true │
│ true │ Bool │ true │ Bool │ true │
│ [1,2,3] │ Array(Int64) │ [1,2,3] │ Array(Int64) │ true │
└─────────┴────────────────┴─────────┴─────────────────┴──────────────────────────────────┘
데이터에서 Dynamic 타입 읽기 (Reading Dynamic type from the data)
모든 텍스트 형식(TSV, CSV, CustomSeparated, Values, JSONEachRow 등)이 Dynamic 타입 읽기를 지원해요. 데이터 파싱 중 ClickHouse는 각 값의 타입을 추론하려고 하며, 그 추론을 Dynamic 컬럼에 넣을 때 사용해요.
예시:
SELECT
d,
dynamicType(d),
dynamicElement(d, 'String') AS str,
dynamicElement(d, 'Int64') AS num,
dynamicElement(d, 'Float64') AS float,
dynamicElement(d, 'Date') AS date,
dynamicElement(d, 'Array(Int64)') AS arr
FROM format(JSONEachRow, 'd Dynamic', $$
{"d" : "Hello, World!"},
{"d" : 42},
{"d" : 42.42},
{"d" : "2020-01-01"},
{"d" : [1, 2, 3]}
$$)
┌─d─────────────┬─dynamicType(d)─┬─str───────────┬──num─┬─float─┬───────date─┬─arr─────┐
│ Hello, World! │ String │ Hello, World! │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │
│ 42 │ Int64 │ ᴺᵁᴸᴸ │ 42 │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │
│ 42.42 │ Float64 │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ 42.42 │ ᴺᵁᴸᴸ │ [] │
│ 2020-01-01 │ Date │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ 2020-01-01 │ [] │
│ [1,2,3] │ Array(Int64) │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [1,2,3] │
└───────────────┴────────────────┴───────────────┴──────┴───────┴────────────┴─────────┘
함수에서 Dynamic 타입 사용 (Using Dynamic type in functions)
대부분의 함수가 Dynamic 타입의 인자를 지원해요. 이 경우 함수는 Dynamic 컬럼 안에 저장된 각 내부 데이터 타입에 대해 별도로 실행돼요. 함수의 결과 타입이 인자 타입에 의존하면 Dynamic 인자로 실행된 함수의 결과는 Dynamic이 돼요. 함수의 결과 타입이 인자 타입에 의존하지 않으면 결과는 Nullable(T)가 되고, 여기서 T는 이 함수의 보통 결과 타입이에요.
예시:
CREATE TABLE test (d Dynamic) ENGINE=Memory;
INSERT INTO test VALUES (NULL), (1::Int8), (2::Int16), (3::Int32), (4::Int64);
SELECT d, dynamicType(d) FROM test;
┌─d────┬─dynamicType(d)─┐
│ ᴺᵁᴸᴸ │ None │
│ 1 │ Int8 │
│ 2 │ Int16 │
│ 3 │ Int32 │
│ 4 │ Int64 │
└──────┴────────────────┘
SELECT d, d + 1 AS res, toTypeName(res), dynamicType(res) FROM test;
┌─d────┬─res──┬─toTypeName(res)─┬─dynamicType(res)─┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Dynamic │ None │
│ 1 │ 2 │ Dynamic │ Int16 │
│ 2 │ 3 │ Dynamic │ Int32 │
│ 3 │ 4 │ Dynamic │ Int64 │
│ 4 │ 5 │ Dynamic │ Int64 │
└──────┴──────┴─────────────────┴──────────────────┘
SELECT d, d + d AS res, toTypeName(res), dynamicType(res) FROM test;
┌─d────┬─res──┬─toTypeName(res)─┬─dynamicType(res)─┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Dynamic │ None │
│ 1 │ 2 │ Dynamic │ Int16 │
│ 2 │ 4 │ Dynamic │ Int32 │
│ 3 │ 6 │ Dynamic │ Int64 │
│ 4 │ 8 │ Dynamic │ Int64 │
└──────┴──────┴─────────────────┴──────────────────┘
SELECT d, d < 3 AS res, toTypeName(res) FROM test;
┌─d────┬──res─┬─toTypeName(res)─┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Nullable(UInt8) │
│ 1 │ 1 │ Nullable(UInt8) │
│ 2 │ 1 │ Nullable(UInt8) │
│ 3 │ 0 │ Nullable(UInt8) │
│ 4 │ 0 │ Nullable(UInt8) │
└──────┴──────┴─────────────────┘
SELECT d, exp2(d) AS res, toTypeName(res) FROM test;
┌─d────┬──res─┬─toTypeName(res)───┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Nullable(Float64) │
│ 1 │ 2 │ Nullable(Float64) │
│ 2 │ 4 │ Nullable(Float64) │
│ 3 │ 8 │ Nullable(Float64) │
│ 4 │ 16 │ Nullable(Float64) │
└──────┴──────┴───────────────────┘
TRUNCATE TABLE test;
INSERT INTO test VALUES (NULL), ('str_1'), ('str_2');
SELECT d, dynamicType(d) FROM test;
┌─d─────┬─dynamicType(d)─┐
│ ᴺᵁᴸᴸ │ None │
│ str_1 │ String │
│ str_2 │ String │
└───────┴────────────────┘
SELECT d, upper(d) AS res, toTypeName(res) FROM test;
┌─d─────┬─res───┬─toTypeName(res)──┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Nullable(String) │
│ str_1 │ STR_1 │ Nullable(String) │
│ str_2 │ STR_2 │ Nullable(String) │
└───────┴───────┴──────────────────┘
SELECT d, extract(d, '([0-3])') AS res, toTypeName(res) FROM test;
┌─d─────┬─res──┬─toTypeName(res)──┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Nullable(String) │
│ str_1 │ 1 │ Nullable(String) │
│ str_2 │ 2 │ Nullable(String) │
└───────┴──────┴──────────────────┘
TRUNCATE TABLE test;
INSERT INTO test VALUES (NULL), ([1, 2]), ([3, 4]);
SELECT d, dynamicType(d) FROM test;
┌─d─────┬─dynamicType(d)─┐
│ ᴺᵁᴸᴸ │ None │
│ [1,2] │ Array(Int64) │
│ [3,4] │ Array(Int64) │
└───────┴────────────────┘
SELECT d, d[1] AS res, toTypeName(res), dynamicType(res) FROM test;
┌─d─────┬─res──┬─toTypeName(res)─┬─dynamicType(res)─┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Dynamic │ None │
│ [1,2] │ 1 │ Dynamic │ Int64 │
│ [3,4] │ 3 │ Dynamic │ Int64 │
└───────┴──────┴─────────────────┴──────────────────┘
함수가 Dynamic 컬럼 안의 어떤 타입에 대해 실행될 수 없으면 예외가 던져져요.
INSERT INTO test VALUES (42), (43), ('str_1');
SELECT d, dynamicType(d) FROM test;
┌─d─────┬─dynamicType(d)─┐
│ 42 │ Int64 │
│ 43 │ Int64 │
│ str_1 │ String │
└───────┴────────────────┘
┌─d─────┬─dynamicType(d)─┐
│ ᴺᵁᴸᴸ │ None │
│ [1,2] │ Array(Int64) │
│ [3,4] │ Array(Int64) │
└───────┴────────────────┘
SELECT d, d + 1 AS res, toTypeName(res), dynamicType(d) FROM test;
Received exception:
Code: 43. DB::Exception: Illegal types Array(Int64) and UInt8 of arguments of function plus: while executing 'FUNCTION plus(__table1.d : 3, 1_UInt8 :: 1) -> plus(__table1.d, 1_UInt8) Dynamic : 0'. (ILLEGAL_TYPE_OF_ARGUMENT)
필요 없는 타입을 걸러낼 수 있어요.
SELECT d, d + 1 AS res, toTypeName(res), dynamicType(res) FROM test WHERE dynamicType(d) NOT IN ('String', 'Array(Int64)', 'None')
┌─d──┬─res─┬─toTypeName(res)─┬─dynamicType(res)─┐
│ 42 │ 43 │ Dynamic │ Int64 │
│ 43 │ 44 │ Dynamic │ Int64 │
└────┴─────┴─────────────────┴──────────────────┘
또는 필요한 타입을 서브컬럼으로 추출해요.
SELECT d, d.Int64 + 1 AS res, toTypeName(res) FROM test;
┌─d─────┬──res─┬─toTypeName(res)─┐
│ 42 │ 43 │ Nullable(Int64) │
│ 43 │ 44 │ Nullable(Int64) │
│ str_1 │ ᴺᵁᴸᴸ │ Nullable(Int64) │
└───────┴──────┴─────────────────┘
┌─d─────┬──res─┬─toTypeName(res)─┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ Nullable(Int64) │
│ [1,2] │ ᴺᵁᴸᴸ │ Nullable(Int64) │
│ [3,4] │ ᴺᵁᴸᴸ │ Nullable(Int64) │
└───────┴──────┴─────────────────┘
타입 불일치 동작 (Type mismatch behavior)
설정 dynamic_throw_on_type_mismatch는 함수가 Dynamic 컬럼에 적용되었을 때, 행의 실제 저장 타입이 그 함수와 호환되지 않는 경우 어떻게 할지를 제어해요.
true(기본값) — 첫 번째 호환되지 않는 행에서 예외(ILLEGAL_TYPE_OF_ARGUMENT)를 던져요.false— 호환되지 않는 행에는NULL을 반환하고, 호환되는 행에는 결과를 유지해요.
예시:
CREATE TABLE test (d Dynamic) ENGINE = Memory;
INSERT INTO test VALUES ('world'), (123), (456);
-- Default (throw on mismatch): length() does not accept integers, so the query throws.
SELECT length(d) FROM test; -- throws ILLEGAL_TYPE_OF_ARGUMENT
-- With throw disabled: incompatible rows return NULL.
SET dynamic_throw_on_type_mismatch = false;
SELECT d, length(d) FROM test ORDER BY d::String NULLS LAST;
┌─d─────┬─length(d)─┐
│ world │ 5 │
│ 123 │ ᴺᵁᴸᴸ │
│ 456 │ ᴺᵁᴸᴸ │
└───────┴───────────┘
ORDER BY와 GROUP BY에서 Dynamic 타입 사용 (Using Dynamic type in ORDER BY and GROUP BY)
ORDER BY와 GROUP BY에서 Dynamic 타입의 값은 Variant 타입의 값과 비슷하게 비교돼요. 하부 타입 T1을 가진 d1과 하부 타입 T2를 가진 d2의 Dynamic 타입 값에 대한 연산자 <의 결과는 다음과 같이 정의돼요.
T1 = T2 = T이면 결과는d1.T < d2.T(하부 값이 비교돼요).T1 != T2이면 결과는T1 < T2(타입 이름이 비교돼요).
기본적으로 Dynamic 타입은 GROUP BY/ORDER BY 키에 허용되지 않아요. 그것을 사용하려면 특별한 비교 규칙을 고려하고 allow_suspicious_types_in_group_by/allow_suspicious_types_in_order_by 설정을 켜야 해요.
예시:
CREATE TABLE test (d Dynamic) ENGINE=Memory;
INSERT INTO test VALUES (42), (43), ('abc'), ('abd'), ([1, 2, 3]), ([]), (NULL);
SELECT d, dynamicType(d) FROM test;
┌─d───────┬─dynamicType(d)─┐
│ 42 │ Int64 │
│ 43 │ Int64 │
│ abc │ String │
│ abd │ String │
│ [1,2,3] │ Array(Int64) │
│ [] │ Array(Int64) │
│ ᴺᵁᴸᴸ │ None │
└─────────┴────────────────┘
SELECT d, dynamicType(d) FROM test ORDER BY d SETTINGS allow_suspicious_types_in_order_by=1;
┌─d───────┬─dynamicType(d)─┐
│ [] │ Array(Int64) │
│ [1,2,3] │ Array(Int64) │
│ 42 │ Int64 │
│ 43 │ Int64 │
│ abc │ String │
│ abd │ String │
│ ᴺᵁᴸᴸ │ None │
└─────────┴────────────────┘
참고: 서로 다른 숫자 타입을 가진 dynamic 타입의 값들은 서로 다른 값으로 간주되어 서로 비교되지 않고, 그들의 타입 이름이 비교돼요.
예시:
CREATE TABLE test (d Dynamic) ENGINE=Memory;
INSERT INTO test VALUES (1::UInt32), (1::Int64), (100::UInt32), (100::Int64);
SELECT d, dynamicType(d) FROM test ORDER BY d SETTINGS allow_suspicious_types_in_order_by=1;
┌─d───┬─dynamicType(d)─┐
│ 1 │ Int64 │
│ 100 │ Int64 │
│ 1 │ UInt32 │
│ 100 │ UInt32 │
└─────┴────────────────┘
SELECT d, dynamicType(d) FROM test GROUP BY d SETTINGS allow_suspicious_types_in_group_by=1;
┌─d───┬─dynamicType(d)─┐
│ 1 │ Int64 │
│ 100 │ UInt32 │
│ 1 │ UInt32 │
│ 100 │ Int64 │
└─────┴────────────────┘
참고: 설명된 비교 규칙은 Dynamic 타입이 있는 함수의 특별한 동작 때문에 </>/= 같은 비교 함수 실행 중에는 적용되지 않아요.
Dynamic 안에 저장된 서로 다른 데이터 타입 수의 한계에 도달 (Reaching the limit in number of different data types stored inside Dynamic)
Dynamic 데이터 타입은 서로 다른 데이터 타입을 별도의 서브컬럼으로 오직 제한된 수만 저장할 수 있어요. 기본적으로 이 한계는 32이지만, 타입 선언에서 Dynamic(max_types=N) 문법으로 바꿀 수 있어요. 여기서 N은 0과 254 사이예요(구현 세부사항 때문에 Dynamic 안에 별도 서브컬럼으로 저장할 수 있는 서로 다른 데이터 타입이 254개를 초과하는 것은 불가능해요). 한계에 도달하면 Dynamic 컬럼에 삽입되는 모든 새 데이터 타입은 서로 다른 데이터 타입의 값을 이진 형식으로 저장하는 단일 공유 데이터 구조에 삽입돼요.
다양한 시나리오에서 한계에 도달하면 어떤 일이 일어나는지 살펴볼게요.
데이터 파싱 중 한계 도달 (Reaching the limit during data parsing)
데이터에서 Dynamic 값 파싱 중 현재 데이터 블록에 대해 한계에 도달하면 모든 새 값이 공유 데이터 구조에 삽입돼요.
SELECT d, dynamicType(d), isDynamicElementInSharedData(d) FROM format(JSONEachRow, 'd Dynamic(max_types=3)', '
{"d" : 42}
{"d" : [1, 2, 3]}
{"d" : "Hello, World!"}
{"d" : "2020-01-01"}
{"d" : ["str1", "str2", "str3"]}
{"d" : {"a" : 1, "b" : [1, 2, 3]}}
')
┌─d──────────────────────┬─dynamicType(d)─────────────────┬─isDynamicElementInSharedData(d)─┐
│ 42 │ Int64 │ false │
│ [1,2,3] │ Array(Int64) │ false │
│ Hello, World! │ String │ false │
│ 2020-01-01 │ Date │ true │
│ ['str1','str2','str3'] │ Array(String) │ true │
│ (1,[1,2,3]) │ Tuple(a Int64, b Array(Int64)) │ true │
└────────────────────────┴────────────────────────────────┴─────────────────────────────────┘
보시다시피 Int64, Array(Int64), String이라는 3개의 서로 다른 데이터 타입을 넣은 후에는 모든 새 타입이 특별한 공유 데이터 구조에 삽입돼요.
MergeTree 테이블 엔진에서 데이터 파트 병합 중 (During merges of data parts in MergeTree table engines)
MergeTree 테이블에서 여러 데이터 파트를 병합하는 동안 결과 데이터 파트의 Dynamic 컬럼은 안에 있는 별도 서브컬럼에 저장할 수 있는 서로 다른 데이터 타입의 한계에 도달할 수 있고, 소스 파트의 모든 타입을 서브컬럼으로 저장하지 못할 수 있어요. 이 경우 ClickHouse가 병합 후 어떤 타입이 별도 서브컬럼으로 유지되고 어떤 타입이 공유 데이터 구조에 들어갈지를 선택해요. 대부분의 경우 ClickHouse는 가장 빈번한 타입을 유지하고 가장 희귀한 타입을 공유 데이터 구조에 저장하려고 하지만, 구현에 따라 달라져요.
이런 병합의 예를 살펴볼게요. 먼저 Dynamic 컬럼이 있는 테이블을 만들고, 서로 다른 타입의 한계를 3으로 설정한 다음 5개의 서로 다른 타입으로 값을 넣어요.
CREATE TABLE test (id UInt64, d Dynamic(max_types=3)) ENGINE=MergeTree ORDER BY id;
SYSTEM STOP MERGES test;
INSERT INTO test SELECT number, number FROM numbers(5);
INSERT INTO test SELECT number, range(number) FROM numbers(4);
INSERT INTO test SELECT number, toDate(number) FROM numbers(3);
INSERT INTO test SELECT number, map(number, number) FROM numbers(2);
INSERT INTO test SELECT number, 'str_' || toString(number) FROM numbers(1);
각 insert는 단일 타입을 가진 Dynamic 컬럼이 있는 별도의 데이터 파트를 만들어요.
SELECT count(), dynamicType(d), isDynamicElementInSharedData(d), _part FROM test GROUP BY _part, dynamicType(d), isDynamicElementInSharedData(d) ORDER BY _part, count();
┌─count()─┬─dynamicType(d)──────┬─isDynamicElementInSharedData(d)─┬─_part─────┐
│ 5 │ UInt64 │ false │ all_1_1_0 │
│ 4 │ Array(UInt64) │ false │ all_2_2_0 │
│ 3 │ Date │ false │ all_3_3_0 │
│ 2 │ Map(UInt64, UInt64) │ false │ all_4_4_0 │
│ 1 │ String │ false │ all_5_5_0 │
└─────────┴─────────────────────┴─────────────────────────────────┴───────────┘
이제 모든 파트를 하나로 병합하고 무슨 일이 일어나는지 살펴볼게요.
SYSTEM START MERGES test;
OPTIMIZE TABLE test FINAL;
SELECT count(), dynamicType(d), isDynamicElementInSharedData(d), _part FROM test GROUP BY _part, dynamicType(d), isDynamicElementInSharedData(d) ORDER BY _part, count() desc;
┌─count()─┬─dynamicType(d)──────┬─isDynamicElementInSharedData(d)─┬─_part─────┐
│ 5 │ UInt64 │ false │ all_1_5_2 │
│ 4 │ Array(UInt64) │ false │ all_1_5_2 │
│ 3 │ Date │ false │ all_1_5_2 │
│ 2 │ Map(UInt64, UInt64) │ true │ all_1_5_2 │
│ 1 │ String │ true │ all_1_5_2 │
└─────────┴─────────────────────┴─────────────────────────────────┴───────────┘
보시다시피 ClickHouse는 가장 빈번한 타입인 UInt64와 Array(UInt64)를 서브컬럼으로 유지하고, 다른 모든 타입을 공유 데이터에 삽입했어요.
Dynamic과 JSONExtract 함수 (JSONExtract functions with Dynamic)
모든 JSONExtract* 함수가 Dynamic 타입을 지원해요.
SELECT JSONExtract('{"a" : [1, 2, 3]}', 'a', 'Dynamic') AS dynamic, dynamicType(dynamic) AS dynamic_type;
┌─dynamic─┬─dynamic_type───────────┐
│ [1,2,3] │ Array(Nullable(Int64)) │
└─────────┴────────────────────────┘
SELECT JSONExtract('{"obj" : {"a" : 42, "b" : "Hello", "c" : [1,2,3]}}', 'obj', 'Map(String, Dynamic)') AS map_of_dynamics, mapApply((k, v) -> (k, dynamicType(v)), map_of_dynamics) AS map_of_dynamic_types
┌─map_of_dynamics──────────────────┬─map_of_dynamic_types────────────────────────────────────┐
│ {'a':42,'b':'Hello','c':[1,2,3]} │ {'a':'Int64','b':'String','c':'Array(Nullable(Int64))'} │
└──────────────────────────────────┴─────────────────────────────────────────────────────────┘
SELECT JSONExtractKeysAndValues('{"a" : 42, "b" : "Hello", "c" : [1,2,3]}', 'Dynamic') AS dynamics, arrayMap(x -> (x.1, dynamicType(x.2)), dynamics) AS dynamic_types
┌─dynamics───────────────────────────────┬─dynamic_types─────────────────────────────────────────────────┐
│ [('a',42),('b','Hello'),('c',[1,2,3])] │ [('a','Int64'),('b','String'),('c','Array(Nullable(Int64))')] │
└────────────────────────────────────────┴───────────────────────────────────────────────────────────────┘
이진 출력 형식 (Binary output format)
RowBinary 형식에서 Dynamic 타입의 값은 다음 형식으로 직렬화돼요.
<binary_encoded_data_type><value_in_binary_format_according_to_the_data_type>