이 타입은 다른 데이터 타입의 유니언을 나타냅니다. Variant(T1, T2, ..., TN) 타입은 이 타입의 각 행이 T1, T2, …, TN 중 하나의 타입 값을 가지거나, 어느 타입에도 해당하지 않는 NULL 값을 가질 수 있음을 의미합니다.
중첩된 타입의 순서는 중요하지 않습니다. 즉, Variant(T1, T2) = Variant(T2, T1)입니다.
중첩된 타입은 Nullable(…), LowCardinality(Nullable(…)), Variant(…) 타입을 제외하면 어떤 타입이든 사용할 수 있습니다.
유사한 타입을 variant로 사용하는 것은 권장되지 않습니다(예: Variant(UInt32, Int64)처럼 서로 다른 숫자 타입이나 Variant(Date, DateTime)처럼 서로 다른 날짜 타입).
이러한 타입의 값을 처리할 때 모호성이 발생할 수 있기 때문입니다. 기본적으로 이런 Variant 타입을 생성하면 예외가 발생하지만, 설정 allow_suspicious_variant_types를 사용해 활성화할 수 있습니다.
테이블 컬럼 정의에서 Variant 타입을 사용하는 방법:
CREATE TABLE test (v Variant(UInt64, String, Array(UInt64))) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('Hello, World!'), ([1, 2, 3]);
SELECT v FROM test;
┌─v─────────────┐
│ ᴺᵁᴸᴸ │
│ 42 │
│ Hello, World! │
│ [1,2,3] │
└───────────────┘
일반 컬럼에서 CAST 사용:
SELECT toTypeName(variant) AS type_name, 'Hello, World!'::Variant(UInt64, String, Array(UInt64)) as variant;
┌─type_name──────────────────────────────┬─variant───────┐
│ Variant(Array(UInt64), String, UInt64) │ Hello, World! │
└────────────────────────────────────────┴───────────────┘
인수에 공통 타입이 없을 때 if/multiIf 함수를 사용합니다(use_variant_as_common_type 설정이 활성화되어 있어야 함):
SET use_variant_as_common_type = 1;
SELECT if(number % 2, number, range(number)) as variant FROM numbers(5);
┌─variant───┐
│ [] │
│ 1 │
│ [0,1] │
│ 3 │
│ [0,1,2,3] │
└───────────┘
SET use_variant_as_common_type = 1;
SELECT multiIf((number % 4) = 0, 42, (number % 4) = 1, [1, 2, 3], (number % 4) = 2, 'Hello, World!', NULL) AS variant FROM numbers(4);
┌─variant───────┐
│ 42 │
│ [1,2,3] │
│ Hello, World! │
│ ᴺᵁᴸᴸ │
└───────────────┘
배열 요소/맵 값에 공통 타입이 없는 경우 ‘array/map’ 함수를 사용합니다(이 기능을 사용하려면 use_variant_as_common_type 설정이 활성화되어 있어야 합니다):
SET use_variant_as_common_type = 1;
SELECT array(range(number), number, 'str_' || toString(number)) as array_of_variants FROM numbers(3);
┌─array_of_variants─┐
│ [[],0,'str_0'] │
│ [[0],1,'str_1'] │
│ [[0,1],2,'str_2'] │
└───────────────────┘
SET use_variant_as_common_type = 1;
SELECT map('a', range(number), 'b', number, 'c', 'str_' || toString(number)) as map_of_variants FROM numbers(3);
┌─map_of_variants───────────────┐
│ {'a':[],'b':0,'c':'str_0'} │
│ {'a':[0],'b':1,'c':'str_1'} │
│ {'a':[0,1],'b':2,'c':'str_2'} │
└───────────────────────────────┘
Variant 타입은 타입 이름을 서브컬럼으로 사용해 Variant 컬럼에서 특정 중첩 타입 하나를 읽을 수 있도록 지원합니다.
따라서 variant Variant(T1, T2, T3) 컬럼이 있으면 variant.T2 구문을 사용해 T2 타입의 서브컬럼을 읽을 수 있습니다.
이 서브컬럼의 타입은 T2가 Nullable 안에 포함될 수 있으면 Nullable(T2)이고, 그렇지 않으면 T2입니다. 또한 이 서브컬럼은
원래 Variant 컬럼과 동일한 크기를 가지며, 원래 Variant 컬럼의 타입이 T2가 아닌 모든 행에는 NULL 값(T2가 Nullable 안에 포함될 수 없으면 빈 값)이 들어갑니다.
Variant 서브컬럼은 variantElement(variant_column, type_name) 함수를 사용해서도 읽을 수 있습니다.
예시:
CREATE TABLE test (v Variant(UInt64, String, Array(UInt64))) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('Hello, World!'), ([1, 2, 3]);
SELECT v, v.String, v.UInt64, v.`Array(UInt64)` FROM test;
┌─v─────────────┬─v.String──────┬─v.UInt64─┬─v.Array(UInt64)─┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │
│ 42 │ ᴺᵁᴸᴸ │ 42 │ [] │
│ Hello, World! │ Hello, World! │ ᴺᵁᴸᴸ │ [] │
│ [1,2,3] │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [1,2,3] │
└───────────────┴───────────────┴──────────┴─────────────────┘
SELECT toTypeName(v.String), toTypeName(v.UInt64), toTypeName(v.`Array(UInt64)`) FROM test LIMIT 1;
┌─toTypeName(v.String)─┬─toTypeName(v.UInt64)─┬─toTypeName(v.Array(UInt64))─┐
│ Nullable(String) │ Nullable(UInt64) │ Array(UInt64) │
└──────────────────────┴──────────────────────┴─────────────────────────────┘
SELECT v, variantElement(v, 'String'), variantElement(v, 'UInt64'), variantElement(v, 'Array(UInt64)') FROM test;
┌─v─────────────┬─variantElement(v, 'String')─┬─variantElement(v, 'UInt64')─┬─variantElement(v, 'Array(UInt64)')─┐
│ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │
│ 42 │ ᴺᵁᴸᴸ │ 42 │ [] │
│ Hello, World! │ Hello, World! │ ᴺᵁᴸᴸ │ [] │
│ [1,2,3] │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [1,2,3] │
└───────────────┴─────────────────────────────┴─────────────────────────────┴────────────────────────────────────┘
각 행에 저장된 variant를 확인하려면 variantType(variant_column) 함수를 사용할 수 있습니다. 이 함수는 각 행의 variant 유형 이름을 나타내는 Enum을 반환합니다(NULL인 행은 'None').
예시:
CREATE TABLE test (v Variant(UInt64, String, Array(UInt64))) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('Hello, World!'), ([1, 2, 3]);
SELECT variantType(v) FROM test;
┌─variantType(v)─┐
│ None │
│ UInt64 │
│ String │
│ Array(UInt64) │
└────────────────┘
SELECT toTypeName(variantType(v)) FROM test LIMIT 1;
┌─toTypeName(variantType(v))──────────────────────────────────────────┐
│ Enum8('None' = -1, 'Array(UInt64)' = 0, 'String' = 1, 'UInt64' = 2) │
└─────────────────────────────────────────────────────────────────────┘
Variant 타입의 컬럼에 대해 수행할 수 있는 변환은 4가지입니다.
String 컬럼을 Variant 컬럼으로 변환하기
String에서 Variant로의 변환은 문자열 값을 파싱해 Variant 타입의 값을 추출하는 방식으로 수행됩니다:
SELECT '42'::Variant(String, UInt64) AS variant, variantType(variant) AS variant_type
┌─variant─┬─variant_type─┐
│ 42 │ UInt64 │
└─────────┴──────────────┘
SELECT '[1, 2, 3]'::Variant(String, Array(UInt64)) as variant, variantType(variant) as variant_type
┌─variant─┬─variant_type──┐
│ [1,2,3] │ Array(UInt64) │
└─────────┴───────────────┘
SELECT CAST(map('key1', '42', 'key2', 'true', 'key3', '2020-01-01'), 'Map(String, Variant(UInt64, Bool, Date))') AS map_of_variants, mapApply((k, v) -> (k, variantType(v)), map_of_variants) AS map_of_variant_types```
┌─map_of_variants─────────────────────────────┬─map_of_variant_types──────────────────────────┐
│ {'key1':42,'key2':true,'key3':'2020-01-01'} │ {'key1':'UInt64','key2':'Bool','key3':'Date'} │
└─────────────────────────────────────────────┴───────────────────────────────────────────────┘
String에서 Variant로 변환할 때 파싱을 비활성화하려면 cast_string_to_dynamic_use_inference 설정을 비활성화하면 됩니다:
SET cast_string_to_variant_use_inference = 0;
SELECT '[1, 2, 3]'::Variant(String, Array(UInt64)) as variant, variantType(variant) as variant_type
┌─variant───┬─variant_type─┐
│ [1, 2, 3] │ String │
└───────────┴──────────────┘
타입이 T인 일반 컬럼은 해당 타입을 포함하는 Variant 컬럼으로 변환할 수 있습니다:
SELECT toTypeName(variant) AS type_name, [1,2,3]::Array(UInt64)::Variant(UInt64, String, Array(UInt64)) as variant, variantType(variant) as variant_name
┌─type_name──────────────────────────────┬─variant─┬─variant_name──┐
│ Variant(Array(UInt64), String, UInt64) │ [1,2,3] │ Array(UInt64) │
└────────────────────────────────────────┴─────────┴───────────────┘
참고: String 타입에서의 변환은 항상 파싱을 거쳐 수행됩니다. 파싱 없이 String 컬럼을 Variant의 String 대안으로 변환해야 하는 경우, 다음과 같이 할 수 있습니다:
SELECT '[1, 2, 3]'::Variant(String)::Variant(String, Array(UInt64), UInt64) as variant, variantType(variant) as variant_type
┌─variant───┬─variant_type─┐
│ [1, 2, 3] │ String │
└───────────┴──────────────┘
Variant 컬럼은 일반 컬럼으로 변환할 수 있습니다. 이 경우 모든 중첩된 variant가 대상 타입으로 변환됩니다:
CREATE TABLE test (v Variant(UInt64, String)) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('42.42');
SELECT v::Nullable(Float64) FROM test;
┌─CAST(v, 'Nullable(Float64)')─┐
│ ᴺᵁᴸᴸ │
│ 42 │
│ 42.42 │
└──────────────────────────────┘
Variant를 다른 Variant로 변환하기
Variant 컬럼은 다른 Variant 컬럼으로 변환할 수 있지만, 대상 Variant 컬럼에 원래 Variant의 모든 중첩 타입이 포함된 경우에만 가능합니다:
CREATE TABLE test (v Variant(UInt64, String)) ENGINE = Memory;
INSERT INTO test VALUES (NULL), (42), ('String');
SELECT v::Variant(UInt64, String, Array(UInt64)) FROM test;
┌─CAST(v, 'Variant(UInt64, String, Array(UInt64))')─┐
│ ᴺᵁᴸᴸ │
│ 42 │
│ String │
└───────────────────────────────────────────────────┘
모든 텍스트 형식(TSV, CSV, CustomSeparated, Values, JSONEachRow 등)에서 Variant 타입을 읽을 수 있습니다. 데이터를 파싱하는 동안 ClickHouse는 값을 가장 적합한 Variant 타입으로 삽입하려고 시도합니다.
예시:
SELECT
v,
variantElement(v, 'String') AS str,
variantElement(v, 'UInt64') AS num,
variantElement(v, 'Float64') AS float,
variantElement(v, 'DateTime') AS date,
variantElement(v, 'Array(UInt64)') AS arr
FROM format(JSONEachRow, 'v Variant(String, UInt64, Float64, DateTime, Array(UInt64))', $$
{"v" : "Hello, World!"},
{"v" : 42},
{"v" : 42.42},
{"v" : "2020-01-01 00:00:00"},
{"v" : [1, 2, 3]}
$$)
┌─v───────────────────┬─str───────────┬──num─┬─float─┬────────────────date─┬─arr─────┐
│ Hello, World! │ Hello, World! │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │
│ 42 │ ᴺᵁᴸᴸ │ 42 │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [] │
│ 42.42 │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ 42.42 │ ᴺᵁᴸᴸ │ [] │
│ 2020-01-01 00:00:00 │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ 2020-01-01 00:00:00 │ [] │
│ [1,2,3] │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ ᴺᵁᴸᴸ │ [1,2,3] │
└─────────────────────┴───────────────┴──────┴───────┴─────────────────────┴─────────┘
Variant 타입의 값은 동일한 Variant 타입의 값과만 비교할 수 있습니다.
기본적으로 비교 연산자는 Variant의 기본 구현을 사용하여
각 variant type에 대해 개별적으로 비교를 수행합니다. 이는 setting use_variant_default_implementation_for_comparisons = 0을 사용해 비활성화할 수 있으며,
이 경우 아래에 설명된 네이티브 Variant 비교 규칙이 사용됩니다. 참고: ORDER BY는 항상 네이티브 비교를 사용합니다.
네이티브 Variant 비교 규칙:
타입 Variant(..., T1, ... T2, ...)에서 내부 타입이 T1인 값 v1과 내부 타입이 T2인 값 v2에 대한 연산자 <의 결과는 다음과 같이 정의됩니다.
T1 = T2 = T이면 결과는 v1.T < v2.T입니다(내부 값이 비교됩니다).
T1 != T2이면 결과는 T1 < T2입니다(타입 이름이 비교됩니다).
예시:
SET allow_suspicious_types_in_order_by = 1;
CREATE TABLE test (v1 Variant(String, UInt64, Array(UInt32)), v2 Variant(String, UInt64, Array(UInt32))) ENGINE=Memory;
INSERT INTO test VALUES (42, 42), (42, 43), (42, 'abc'), (42, [1, 2, 3]), (42, []), (42, NULL);
SELECT v2, variantType(v2) AS v2_type FROM test ORDER BY v2;
┌─v2──────┬─v2_type───────┐
│ [] │ Array(UInt32) │
│ [1,2,3] │ Array(UInt32) │
│ abc │ String │
│ 42 │ UInt64 │
│ 43 │ UInt64 │
│ ᴺᵁᴸᴸ │ None │
└─────────┴───────────────┘
SELECT v1, variantType(v1) AS v1_type, v2, variantType(v2) AS v2_type, v1 = v2, v1 < v2, v1 > v2 FROM test;
┌─v1─┬─v1_type─┬─v2──────┬─v2_type───────┬─equals(v1, v2)─┬─less(v1, v2)─┬─greater(v1, v2)─┐
│ 42 │ UInt64 │ 42 │ UInt64 │ 1 │ 0 │ 0 │
│ 42 │ UInt64 │ 43 │ UInt64 │ 0 │ 1 │ 0 │
│ 42 │ UInt64 │ abc │ String │ 0 │ 0 │ 1 │
│ 42 │ UInt64 │ [1,2,3] │ Array(UInt32) │ 0 │ 0 │ 1 │
│ 42 │ UInt64 │ [] │ Array(UInt32) │ 0 │ 0 │ 1 │
│ 42 │ UInt64 │ ᴺᵁᴸᴸ │ None │ 0 │ 1 │ 0 │
└────┴─────────┴─────────┴───────────────┴────────────────┴──────────────┴─────────────────┘
특정 Variant 값을 가진 행을 찾아야 한다면, 다음 방법 중 하나를 사용할 수 있습니다:
- 값을 해당
Variant 유형으로 CAST합니다:
SELECT * FROM test WHERE v2 == [1,2,3]::Array(UInt32)::Variant(String, UInt64, Array(UInt32));
┌─v1─┬─v2──────┐
│ 42 │ [1,2,3] │
└────┴─────────┘
Variant 서브컬럼을 필요한 타입과 비교합니다:
SELECT * FROM test WHERE v2.`Array(UInt32)` == [1,2,3] -- 또는 variantElement(v2, 'Array(UInt32)') 사용
┌─v1─┬─v2──────┐
│ 42 │ [1,2,3] │
└────┴─────────┘
Array/Map/Tuple 같은 복합 타입의 서브컬럼은 Nullable 안에 포함될 수 없으며, 타입이 다른 행에서는 NULL 대신 기본값을 가지므로, 경우에 따라 variant 타입에 추가 검사를 수행하는 것이 유용할 수 있습니다:
SELECT v2, v2.`Array(UInt32)`, variantType(v2) FROM test WHERE v2.`Array(UInt32)` == [];
┌─v2───┬─v2.Array(UInt32)─┬─variantType(v2)─┐
│ 42 │ [] │ UInt64 │
│ 43 │ [] │ UInt64 │
│ abc │ [] │ String │
│ [] │ [] │ Array(UInt32) │
│ ᴺᵁᴸᴸ │ [] │ None │
└──────┴──────────────────┴─────────────────┘
SELECT v2, v2.`Array(UInt32)`, variantType(v2) FROM test WHERE variantType(v2) == 'Array(UInt32)' AND v2.`Array(UInt32)` == [];
┌─v2─┬─v2.Array(UInt32)─┬─variantType(v2)─┐
│ [] │ [] │ Array(UInt32) │
└────┴──────────────────┴─────────────────┘
참고: 숫자 타입이 서로 다른 Variant 값은 서로 다른 Variant로 간주되며, 서로 비교하지 않고 대신 타입 이름을 비교합니다.
예시:
SET allow_suspicious_variant_types = 1;
CREATE TABLE test (v Variant(UInt32, Int64)) ENGINE=Memory;
INSERT INTO test VALUES (1::UInt32), (1::Int64), (100::UInt32), (100::Int64);
SELECT v, variantType(v) FROM test ORDER by v;
┌─v───┬─variantType(v)─┐
│ 1 │ Int64 │
│ 100 │ Int64 │
│ 1 │ UInt32 │
│ 100 │ UInt32 │
└─────┴────────────────┘
참고 기본적으로 Variant 타입은 GROUP BY/ORDER BY 키로 사용할 수 없습니다. 사용하려면 이 타입의 특수한 비교 규칙을 고려하고 allow_suspicious_types_in_group_by/allow_suspicious_types_in_order_by 설정을 활성화하십시오.
모든 JSONExtract* 함수는 Variant 타입을 지원합니다.
SELECT JSONExtract('{"a" : [1, 2, 3]}', 'a', 'Variant(UInt32, String, Array(UInt32))') AS variant, variantType(variant) AS variant_type;
┌─variant─┬─variant_type──┐
│ [1,2,3] │ Array(UInt32) │
└─────────┴───────────────┘
SELECT JSONExtract('{"obj" : {"a" : 42, "b" : "Hello", "c" : [1,2,3]}}', 'obj', 'Map(String, Variant(UInt32, String, Array(UInt32)))') AS map_of_variants, mapApply((k, v) -> (k, variantType(v)), map_of_variants) AS map_of_variant_types
┌─map_of_variants──────────────────┬─map_of_variant_types────────────────────────────┐
│ {'a':42,'b':'Hello','c':[1,2,3]} │ {'a':'UInt32','b':'String','c':'Array(UInt32)'} │
└──────────────────────────────────┴─────────────────────────────────────────────────┘
SELECT JSONExtractKeysAndValues('{"a" : 42, "b" : "Hello", "c" : [1,2,3]}', 'Variant(UInt32, String, Array(UInt32))') AS variants, arrayMap(x -> (x.1, variantType(x.2)), variants) AS variant_types
┌─variants───────────────────────────────┬─variant_types─────────────────────────────────────────┐
│ [('a',42),('b','Hello'),('c',[1,2,3])] │ [('a','UInt32'),('b','String'),('c','Array(UInt32)')] │
└────────────────────────────────────────┴───────────────────────────────────────────────────────┘
ClickHouse의 대부분의 함수는 Variant에 대한 기본 구현을 통해 Variant 타입 인수를 자동으로 지원합니다.
26.1 버전부터는 Variant 타입을 명시적으로 처리하지 않는 함수에 Variant 컬럼이 전달되면 ClickHouse는 다음을 수행합니다:
- Variant 컬럼에서 각 변형 타입을 추출합니다
- 각 변형 타입에 대해 함수를 개별적으로 실행합니다
- 결과 타입에 따라 결과를 적절히 결합합니다
따라서 별도의 특별한 처리 없이도 일반 함수를 Variant 컬럼에 사용할 수 있습니다.
예시:
CREATE TABLE test (v Variant(UInt32, String)) ENGINE = Memory;
INSERT INTO test VALUES (42), ('hello'), (NULL);
SELECT *, toTypeName(v) FROM test WHERE v = 42;
┌─v──┬─toTypeName(v)───────────┐
1. │ 42 │ Variant(String, UInt32) │
└────┴─────────────────────────┘
비교 연산자는 각 variant 타입에 자동으로 개별 적용되므로 Variant 컬럼을 필터링할 수 있습니다.
결과 타입 동작:
결과 타입은 함수가 각 variant에 대해 반환하는 값에 따라 달라집니다.
-
서로 다른 결과 타입:
Variant(T1, T2, ...)
CREATE TABLE test2 (v Variant(UInt64, Float64)) ENGINE = Memory;
INSERT INTO test2 VALUES (42::UInt64), (42.42);
SELECT v + 1 AS result, toTypeName(result) FROM test2;
┌─result─┬─toTypeName(plus(v, 1))──┐
│ 43 │ Variant(Float64, UInt64) │
│ 43.42 │ Variant(Float64, UInt64) │
└────────┴─────────────────────────┘
-
타입 비호환성: 호환되지 않는 variant에는
NULL
CREATE TABLE test3 (v Variant(Array(UInt32), UInt32)) ENGINE = Memory;
INSERT INTO test3 VALUES ([1,2,3]), (42);
SELECT v + 10 AS result, toTypeName(result) FROM test3;
┌─result─┬─toTypeName(plus(v, 10))─┐
│ ᴺᵁᴸᴸ │ Nullable(UInt64) │
│ 52 │ Nullable(UInt64) │
└────────┴─────────────────────────┘
오류 처리: 함수가 variant 타입을 처리할 수 없을 때는 타입 관련 오류(ILLEGAL_TYPE_OF_ARGUMENT,
TYPE_MISMATCH, CANNOT_CONVERT_TYPE, NO_COMMON_TYPE)만 포착되며, 해당 행의 결과는 NULL이 됩니다. 그 밖의 오류(예:
0으로 나누기, 메모리 부족)는 실제 문제를 조용히 숨기지 않도록 평소처럼 그대로 발생합니다.
설정 variant_throw_on_type_mismatch는 함수가 Variant 컬럼에 적용될 때, 행에 실제로 저장된 유형이 해당 함수와 호환되지 않으면 어떻게 처리할지를 제어합니다:
true (기본값) — 처음으로 호환되지 않는 행에서 예외(ILLEGAL_TYPE_OF_ARGUMENT)를 발생시킵니다.
false — 호환되지 않는 행에는 NULL을 반환하고, 호환되는 행은 결과를 유지합니다.
예시:
CREATE TABLE test (v Variant(String, UInt64)) ENGINE = Memory;
INSERT INTO test VALUES ('hello'), (42), ('foo');
-- 기본값 (불일치 시 예외 발생): length()는 UInt64를 허용하지 않으므로 쿼리에서 예외가 발생합니다.
SELECT length(v) FROM test; -- ILLEGAL_TYPE_OF_ARGUMENT 예외 발생
-- throw 비활성화 시: 호환되지 않는 행은 NULL을 반환합니다.
SET variant_throw_on_type_mismatch = false;
SELECT v, length(v) FROM test ORDER BY v::String NULLS LAST;
┌─v─────┬─length(v)─┐
│ foo │ 3 │
│ hello │ 5 │
│ 42 │ ᴺᵁᴸᴸ │
└───────┴───────────┘