Struct 데이터 타입
Struct 데이터 타입
개념적으로 STRUCT 컬럼은 "entries"라고 불리는 컬럼들의 정렬된 목록을 담아요. entries는 문자열을 사용해 이름으로 참조되며, 이 문서에서는 그 entry 이름을 **키(key)**라 부를게요. STRUCT 컬럼의 각 행은 같은 키를 가져야 하고, struct entry의 이름은 스키마의 일부예요. entry 이름은 대소문자를 구분하지 않아요.
출처: 문서
본문
STRUCT는 보통 여러 컬럼을 단일 컬럼으로 중첩하는 데 사용돼요. 중첩된 컬럼은 다른 STRUCT와 LIST를 포함해 어떤 타입이든 될 수 있어요.
STRUCT는 PostgreSQL의 ROW 타입과 비슷해요. 핵심 차이는 DuckDB STRUCT가 STRUCT 컬럼의 각 행에서 같은 키를 요구한다는 점이에요. 이를 통해 DuckDB는 벡터화된 실행 엔진을 완전히 활용해 상당한 성능 향상을 제공하고, 정확성을 위한 타입 일관성도 강제할 수 있어요. DuckDB에는 STRUCT를 만드는 특별한 방법으로 row 함수가 있지만, ROW 데이터 타입은 없어요. 아래 예시와 STRUCT 함수 문서를 참고해 주세요.
중첩 데이터 타입 간 비교는 데이터 타입 개요를 참고해 주세요.
Struct 만들기
Struct는 struct_pack(name := expr, ...) 함수, 동등한 배열 표기 {'name': expr, ...}, 행 변수, 또는 row 함수를 사용해 만들 수 있어요.
struct_pack 함수로 struct 만들기. 키 주위에 단일 따옴표가 없고 := 연산자를 쓴다는 점에 주목하세요.
SELECT struct_pack(key1 := 'value1', key2 := 42) AS s;
배열 표기로 struct 만들기:
SELECT {'key1': 'value1', 'key2': 42} AS s;
행 변수로 struct 만들기:
SELECT d AS s FROM (SELECT 'value1' AS key1, 42 AS key2) d;
정수 struct 만들기:
SELECT {'x': 1, 'y': 2, 'z': 3} AS s;
NULL 값을 가진 문자열 struct 만들기:
SELECT {'yes': 'duck', 'maybe': 'goose', 'huh': NULL, 'no': 'heron'} AS s;
키마다 다른 타입의 struct 만들기:
SELECT {'key1': 'string', 'key2': 1, 'key3': 12.345} AS s;
NULL 값을 가진 struct의 struct 만들기:
SELECT {
'birds': {'yes': 'duck', 'maybe': 'goose', 'huh': NULL, 'no': 'heron'},
'aliens': NULL,
'amphibians': {'yes': 'frog', 'maybe': 'salamander', 'huh': 'dragon', 'no': 'toad'}
} AS s;
Struct 필드 추가 또는 업데이트
새 필드를 추가하거나 기존 필드를 업데이트하려면 struct_update를 쓸 수 있어요.
SELECT struct_update({'a': 1, 'b': 2}, b := 3, c := 4) AS s;
대안으로 struct_insert도 새 필드 추가를 허용하지만 기존 필드 업데이트는 안 돼요.
Struct에서 값 꺼내기
struct에서 값을 꺼내는 것은 점 표기(dot notation), 대괄호 표기(bracket notation), 또는 struct_extract 같은 struct 함수를 통해 할 수 있어요.
점 표기로 키 위치의 값을 꺼내기. 다음 쿼리에서 서브쿼리가 struct 컬럼 a를 만들고, 이를 a.x로 쿼리해요.
SELECT a.x FROM (SELECT {'x': 1, 'y': 2, 'z': 3} AS a);
키에 공백이 있으면 큰따옴표(")로 감싸면 돼요.
SELECT a."x space" FROM (SELECT {'x space': 1, 'y': 2, 'z': 3} AS a);
대괄호 표기도 사용할 수 있어요. 특정 문자열 키를 지정하는 것이 목표이고 대괄호 안에는 상수 표현식만 쓸 수 있으므로 단일 따옴표(')를 사용한다는 점에 주의하세요 (표현식은 불가).
SELECT a['x space'] FROM (SELECT {'x space': 1, 'y': 2, 'z': 3} AS a);
struct_extract 함수도 동등해요. 이 값은 1을 반환해요.
SELECT struct_extract({'x space': 1, 'y': 2, 'z': 3}, 'x space');
unnest / STRUCT.*
struct에서 단일 키를 꺼내는 대신, unnest 특수 함수를 사용하면 struct의 모든 키를 별도의 컬럼으로 꺼낼 수 있어요. 이전 연산이 모양을 알 수 없는 struct를 만들거나, 쿼리가 어떤 잠재적 struct 키든 처리해야 할 때 특히 유용하답니다.
SELECT unnest(a)
FROM (SELECT {'x': 1, 'y': 2, 'z': 3} AS a);
| x | y | z |
|---|---|---|
| 1 | 2 | 3 |
같은 결과를 스타 표기(*)로도 얻을 수 있는데, 이는 반환된 컬럼의 수정을 추가로 허용해요.
SELECT a.* EXCLUDE ('y')
FROM (SELECT {'x': 1, 'y': 2, 'z': 3} AS a);
| x | z |
|---|---|
| 1 | 3 |
경고 — 스타 표기는 현재 최상위 struct 컬럼과 비-집계 표현식으로 제한돼요.
점 표기 연산 순서
struct를 점 표기로 참조하는 것은 스키마와 테이블을 참조하는 것과 모호할 수 있어요. 일반적으로 DuckDB는 먼저 컬럼을 찾고, 그다음 컬럼 안의 struct 키를 찾아요. DuckDB는 다음 순서로 참조를 해석하며 첫 번째 일치를 사용해요.
점 없음
SELECT part1
FROM tbl;
part1은 컬럼
점 하나
SELECT part1.part2
FROM tbl;
part1은 테이블,part2는 컬럼part1은 컬럼,part2는 그 컬럼의 프로퍼티
점 두 개 (이상)
SELECT part1.part2.part3
FROM tbl;
part1은 스키마,part2는 테이블,part3는 컬럼part1은 테이블,part2는 컬럼,part3는 그 컬럼의 프로퍼티part1은 컬럼,part2는 그 컬럼의 프로퍼티,part3는 그 컬럼의 프로퍼티
추가적인 부분들(예: .part4.part5 등)은 항상 프로퍼티로 취급돼요.
row 함수로 Struct 만들기
row 함수는 여러 컬럼을 자동으로 단일 struct 컬럼으로 변환하는 데 쓰일 수 있어요. row를 사용하면 키가 빈 문자열이 되어 struct 컬럼이 있는 테이블에 쉽게 삽입할 수 있어요. 다만 컬럼은 row 함수로 초기화할 수 없고 명시적으로 이름을 지어야 해요.
예를 들어 row 함수로 struct 컬럼에 값을 삽입하는 경우:
CREATE TABLE t1 (s STRUCT(v VARCHAR, i INTEGER));
INSERT INTO t1 VALUES (row('a', 42));
SELECT * FROM t1;
테이블은 단일 entry를 포함해요.
{'v': a, 'i': 42}
다음은 위와 같은 결과를 만듭니다.
CREATE TABLE t1 AS (
SELECT row('a', 42)::STRUCT(v VARCHAR, i INTEGER)
);
row 함수로 struct 컬럼을 초기화하면 실패해요.
CREATE TABLE t2 AS SELECT row('a');
Invalid Input Error:
A table cannot be created from an unnamed struct
struct 간 캐스팅 시 적어도 하나의 필드 이름이 일치해야 해요. 그래서 다음 쿼리는 실패합니다.
SELECT a::STRUCT(y INTEGER) AS b
FROM
(SELECT {'x': 42} AS a);
Binder Error:
STRUCT to STRUCT cast must have at least one matching member
이에 대한 우회 방법은 대신 struct_pack을 사용하는 거예요.
SELECT struct_pack(y := a.x) AS b
FROM
(SELECT {'x': 42} AS a);
row 함수는 unnamed struct를 반환하는 데 쓸 수 있어요. 예를 들어:
SELECT row(x, x + 1, y) FROM (SELECT 1 AS x, 'a' AS y) AS s;
이것은 (1, 2, a)를 만들어요.
struct를 만들 때 여러 표현식을 사용하면 row 함수는 선택적이에요. 다음 쿼리는 이전 것과 같은 결과를 반환해요.
SELECT (x, x + 1, y) AS s FROM (SELECT 1 AS x, 'a' AS y);
비교와 정렬
STRUCT 타입은 모든 비교 연산자를 사용해 비교할 수 있어요. 이러한 비교는 WHERE와 HAVING 절 같은 논리 표현식에서 쓸 수 있고, BOOLEAN 값을 반환해요.
비교는 사전식 순서로 수행되며, 개별 entry는 평소처럼 비교되되 NULL 값은 다른 모든 값보다 큰 것으로 취급돼요.
구체적으로:
s1과s2의 모든 값이 같게 비교되면s1과s2는 같게 비교돼요.- 그렇지 않으면,
s1.value[i] != s2.value[i]인 첫 인덱스i에 대해s1.value[i] < s2.value[i] OR s2.value[i] is NULL이면s1이s2보다 작아요 (그 반대도 마찬가지).
서로 다른 타입의 struct는 관련된 키의 합집합을 가진 struct 타입으로 암묵적으로 캐스팅돼요 (결합 캐스팅 규칙을 따름).
다음 쿼리들은 true를 반환해요.
SELECT {'k1': 0, 'k2': 0} < {'k1': 1, 'k2': 0};
SELECT {'k1': 'hello'} < {'k1': 'world'};
SELECT {'k1': 0, 'k2': 0} < {'k1': 0, 'k2': NULL};
SELECT {'k1': 0} < {'k2': 0};
SELECT {'k1': 0, 'k2': 0} < {'k2': 0, 'k3': 0};
SELECT {'k1': 1, 'k2': 0} > {'k3': 0, 'k1': 0};
다음 쿼리들은 false를 반환해요.
SELECT {'k1': 1, 'k2': 0} < {'k1': 0, 'k2': 1};
SELECT {'k1': [0]} < {'k1': [0, 0]};
SELECT {'k1': 1} > {'k2': 0};
SELECT {'k1': 0, 'k2': 0} < {'k3': 0, 'k1': 1};
SELECT {'k1': 1, 'k2': 0} > {'k2': 0, 'k3': 0};
스키마 업데이트
DuckDB v1.3.0부터 ALTER TABLE 절을 사용해 struct의 하위 스키마를 업데이트할 수 있어요.
예시를 따라오려면 test 테이블을 다음과 같이 초기화하세요.
CREATE TABLE test (s STRUCT(i INTEGER, j INTEGER));
INSERT INTO test VALUES (ROW(1, 1)), (ROW(2, 2));
필드 추가
테이블 test의 struct s에 필드 k INTEGER 추가:
ALTER TABLE test ADD COLUMN s.k INTEGER;
FROM test;
┌─────────────────────────────────────────┐
│ s │
│ struct(i integer, j integer, k integer) │
├─────────────────────────────────────────┤
│ {'i': 1, 'j': 1, 'k': NULL} │
│ {'i': 2, 'j': 2, 'k': NULL} │
└─────────────────────────────────────────┘
필드 제거
테이블 test의 struct s에서 필드 i 제거:
ALTER TABLE test DROP COLUMN s.i;
FROM test;
┌──────────────────────────────┐
│ s │
│ struct(j integer, k integer) │
├──────────────────────────────┤
│ {'j': 1, 'k': NULL} │
│ {'j': 2, 'k': NULL} │
└──────────────────────────────┘
필드 이름 변경
테이블 test의 struct s의 필드 j를 v1로 이름 변경:
ALTER TABLE test RENAME s.j TO v1;
FROM test;
┌───────────────────────────────┐
│ s │
│ struct(v1 integer, k integer) │
├───────────────────────────────┤
│ {'v1': 1, 'k': NULL} │
│ {'v1': 2, 'k': NULL} │
└───────────────────────────────┘
함수
Struct 함수 문서를 참고해 주세요.
더 알아보기 (Learn more)
- struct 함수는
sql/functions/struct문서를 참고해 주세요.