Struct 데이터 타입

Struct 데이터 타입

개념적으로 STRUCT 컬럼은 "entries"라고 불리는 컬럼들의 정렬된 목록을 담아요. entries는 문자열을 사용해 이름으로 참조되며, 이 문서에서는 그 entry 이름을 **키(key)**라 부를게요. STRUCT 컬럼의 각 행은 같은 키를 가져야 하고, struct entry의 이름은 스키마의 일부예요. entry 이름은 대소문자를 구분하지 않아요.

출처: 문서

본문

STRUCT는 보통 여러 컬럼을 단일 컬럼으로 중첩하는 데 사용돼요. 중첩된 컬럼은 다른 STRUCTLIST를 포함해 어떤 타입이든 될 수 있어요.

STRUCT는 PostgreSQL의 ROW 타입과 비슷해요. 핵심 차이는 DuckDB STRUCTSTRUCT 컬럼의 각 행에서 같은 키를 요구한다는 점이에요. 이를 통해 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;
  1. part1은 컬럼

점 하나

SELECT part1.part2
FROM tbl;
  1. part1은 테이블, part2는 컬럼
  2. part1은 컬럼, part2는 그 컬럼의 프로퍼티

점 두 개 (이상)

SELECT part1.part2.part3
FROM tbl;
  1. part1은 스키마, part2는 테이블, part3는 컬럼
  2. part1은 테이블, part2는 컬럼, part3는 그 컬럼의 프로퍼티
  3. 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 타입은 모든 비교 연산자를 사용해 비교할 수 있어요. 이러한 비교는 WHEREHAVING 절 같은 논리 표현식에서 쓸 수 있고, BOOLEAN 값을 반환해요.

비교는 사전식 순서로 수행되며, 개별 entry는 평소처럼 비교되되 NULL 값은 다른 모든 값보다 큰 것으로 취급돼요.

구체적으로:

  • s1s2의 모든 값이 같게 비교되면 s1s2는 같게 비교돼요.
  • 그렇지 않으면, s1.value[i] != s2.value[i]인 첫 인덱스 i에 대해 s1.value[i] < s2.value[i] OR s2.value[i] is NULL이면 s1s2보다 작아요 (그 반대도 마찬가지).

서로 다른 타입의 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의 필드 jv1로 이름 변경:

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 문서를 참고해 주세요.