DuckDB와 Python 간 변환

DuckDB와 Python 간 변환 (Conversion between DuckDB and Python)

이 페이지에서는 Python 객체를 DuckDB로 변환하는 규칙과 DuckDB 결과를 Python으로 변환하는 규칙을 정리해 드릴게요.

출처: 문서

본문

객체 변환: Python 객체 → DuckDB

다음은 Python 객체 타입을 DuckDB [Logical Types]({% link docs/current/sql/data_types/overview.md %})로 매핑한 표예요.

  • NoneNULL
  • boolBOOLEAN
  • datetime.timedeltaINTERVAL
  • strVARCHAR
  • bytearrayBLOB
  • memoryviewBLOB
  • decimal.DecimalDECIMAL / DOUBLE
  • uuid.UUIDUUID

나머지 변환 규칙은 다음과 같아요.

int

Python의 정수는 임의의 크기를 가질 수 있으므로 int에 대해 일대일 변환은 불가능해요. 대신 다음 캐스트를 순서대로 시도해서 하나가 성공할 때까지 진행해요.

  • BIGINT
  • INTEGER
  • UBIGINT
  • UINTEGER
  • DOUBLE

DuckDB Value 클래스를 사용할 때는 대상 타입을 설정할 수 있는데, 이게 변환에 영향을 줘요.

float

이 캐스트들을 순서대로 시도해 하나가 성공할 때까지 진행해요.

  • DOUBLE
  • FLOAT

datetime.datetime

datetime에 대해 pandas.isnull을 사용할 수 있으면 확인해서 true가 나오면 NULL을 반환해요. datetime.datetime.mindatetime.datetime.max를 확인해 각각 -inf+inf로 변환해요.

datetime에 tzinfo가 있으면 TIMESTAMPTZ를, 그렇지 않으면 TIMESTAMP를 사용해요.

datetime.time

time에 tzinfo가 있으면 TIMETZ를, 그렇지 않으면 TIME을 사용해요.

datetime.date

dateDATE 타입으로 변환돼요. datetime.date.mindatetime.date.max를 확인해 각각 -inf+inf로 변환해요.

bytes

bytes는 기본적으로 BLOB으로 변환되지만, BITSTRING 타입의 Value 객체를 만들 때 사용하면 대신 BITSTRING으로 매핑돼요.

list

list는 자식들의 "가장 허용적인" 타입의 LIST 타입이 돼요. 예를 들어:

my_list_value = [
    12345,
    "test"
]

12345는 VARCHAR로 변환할 수 있지만 testINTEGER로 변환할 수 없으므로 VARCHAR[]가 돼요.

[12345, test]

dict

dict 객체는 구조에 따라 STRUCT(...) 또는 MAP(..., ...)으로 변환될 수 있어요. dict의 구조가 다음과 비슷하면:

import duckdb

my_map_dict = {
    "key": [
        1, 2, 3
    ],
    "value": [
        "one", "two", "three"
    ]
}

duckdb.values(my_map_dict)

두 리스트를 함께 지퍼(zipped)한 키-값 쌍의 MAP으로 변환돼요. 위 예시는 MAP(INTEGER, VARCHAR)이 돼요.

┌─────────────────────────┐
│ {1=one, 2=two, 3=three} │
│  map(integer, varchar)  │
├─────────────────────────┤
│ {1=one, 2=two, 3=three} │
└─────────────────────────┘

dict가 [함수]({% link docs/current/clients/python/function.md %})에 의해 반환되면 그 함수는 MAP을 반환하므로, 함수의 return_type을 지정해야 해요. MAP으로 변환할 수 없는 반환 타입을 제공하면 오류가 발생해요.

import duckdb
duckdb_conn = duckdb.connect()

def get_map() -> dict[str,list[str]|list[int]]:
    return {
        "key": [
            1, 2, 3
        ],
        "value": [
            "one", "two", "three"
        ]
    }

duckdb_conn.create_function("get_map", get_map, return_type=dict[int, str])

duckdb_conn.sql("select get_map()").show()

duckdb_conn.create_function("get_map_error", get_map)

duckdb_conn.sql("select get_map_error()").show()
┌─────────────────────────┐
│        get_map()        │
│  map(bigint, varchar)   │
├─────────────────────────┤
│ {1=one, 2=two, 3=three} │
└─────────────────────────┘

ConversionException: Conversion Error: Type VARCHAR can't be cast as UNION(u1 VARCHAR[], u2 BIGINT[]). VARCHAR can't be implicitly cast to any of the union member types: VARCHAR[], BIGINT[]

필드의 이름이 중요하고, 두 리스트는 같은 크기여야 해요.

그렇지 않으면 STRUCT로 변환을 시도해요.

import duckdb

my_struct_dict = {
    1: "one",
    "2": 2,
    "three": [1, 2, 3],
    False: True
}

duckdb.values(my_struct_dict)

변환됩니다:

┌────────────────────────────────────────────────────────────────────┐
│      {'1': 'one', '2': 2, 'three': [1, 2, 3], 'False': true}       │
│ struct("1" varchar, "2" integer, three integer[], "false" boolean) │
├────────────────────────────────────────────────────────────────────┤
│ {'1': one, '2': 2, 'three': [1, 2, 3], 'False': true}              │
└────────────────────────────────────────────────────────────────────┘

dict가 [함수]({% link docs/current/clients/python/function.md %})에 의해 반환되면 그 함수는 [자동 변환]({% link docs/current/clients/python/types.md %}#dictkey_type-value_type) 때문에 MAP을 반환해요. STRUCT를 반환하려면 return_type을 제공해야 해요.

import duckdb
from duckdb.sqltypes import BOOLEAN, INTEGER, VARCHAR
from duckdb import list_type, struct_type

duckdb_conn = duckdb.connect()

my_struct_dict = {
    1: "one",
    "2": 2,
    "three": [1, 2, 3],
    False: True
}

def get_struct() -> dict[str|int|bool,str|int|list[int]|bool]:
    return my_struct_dict

duckdb_conn.create_function("get_struct_as_map", get_struct)

duckdb_conn.sql("select get_struct_as_map()").show()

duckdb_conn.create_function("get_struct", get_struct, return_type=struct_type({
    1: VARCHAR,
    "2": INTEGER,
    "three": list_type(INTEGER),
    False: BOOLEAN
}))

duckdb_conn.sql("select get_struct()").show()
┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                         get_struct_as_map()                                          │
│ map(union(u1 varchar, u2 bigint, u3 boolean), union(u1 varchar, u2 bigint, u3 bigint[], u4 boolean)) │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ {1=one, 2=2, three=[1, 2, 3], false=true}                                                            │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                            get_struct()                            │
│ struct("1" varchar, "2" integer, three integer[], "false" boolean) │
├────────────────────────────────────────────────────────────────────┤
│ {'1': one, '2': 2, 'three': [1, 2, 3], 'False': true}              │
└────────────────────────────────────────────────────────────────────┘

사전의 모든 key는 문자열로 변환돼요.

tuple

tuple은 기본적으로 LIST로 변환되지만, STRUCT 타입의 Value 객체를 만들 때 사용하면 대신 STRUCT로 변환돼요.

numpy.ndarraynumpy.datetime64

ndarraydatetime64tolist()를 호출해 그 결과를 변환하는 방식으로 변환돼요.

결과 변환: DuckDB 결과 → Python

DuckDB의 Python 클라이언트는 데이터를 효율적으로 가져오는 데 사용할 수 있는 여러 추가 메서드를 제공해요.

NumPy

  • fetchnumpy() — 데이터를 NumPy 배열 사전으로 가져와요.

Pandas

  • df() — 데이터를 Pandas DataFrame으로 가져와요.
  • fetchdf()df()의 별칭이에요.
  • fetch_df()df()의 별칭이에요.
  • fetch_df_chunk(vector_multiple) — 결과의 일부를 DataFrame으로 가져와요. 각 청크에서 반환되는 행 수는 벡터 크기(기본 2048) × vector_multiple(기본 1)이에요.

Apache Arrow

Deprecated fetch_arrow_table()fetch_record_batch()는 deprecated예요. 대신 to_arrow_table()to_arrow_reader()를 사용하세요.

Polars

  • pl() — 데이터를 Polars DataFrame으로 가져와요.

예시

이 기능을 사용하는 몇 가지 예시를 볼게요. 더 많은 예시는 [Python guides]({% link docs/current/guides/overview.md %}#python-client)를 참고하세요.

Pandas DataFrame으로 가져오기:

df = con.execute("SELECT * FROM items").fetchdf()
print(df)
       item   value  count
0     jeans    20.0      1
1    hammer    42.2      2
2    laptop  2000.0      1
3  chainsaw   500.0     10
4    iphone   300.0      2

NumPy 배열 사전으로 가져오기:

arr = con.execute("SELECT * FROM items").fetchnumpy()
print(arr)
{'item': masked_array(data=['jeans', 'hammer', 'laptop', 'chainsaw', 'iphone'],
             mask=[False, False, False, False, False],
       fill_value='?',
            dtype=object), 'value': masked_array(data=[20.0, 42.2, 2000.0, 500.0, 300.0],
             mask=[False, False, False, False, False],
       fill_value=1e+20), 'count': masked_array(data=[1, 2, 1, 10, 2],
             mask=[False, False, False, False, False],
       fill_value=999999,
            dtype=int32)}

Arrow table로 가져오기. 이후 예쁘게 출력하기 위해 Pandas로 변환한 것뿐이에요.

tbl = con.execute("SELECT * FROM items").to_arrow_table()
print(tbl.to_pandas())
       item    value  count
0     jeans    20.00      1
1    hammer    42.20      2
2    laptop  2000.00      1
3  chainsaw   500.00     10
4    iphone   300.00      2

더 알아보기 (Learn more)