Python·Arrow·Polars·BI
Python API, Arrow zero-copy, Pandas/Polars replacement scan, BI 연결을 실무 분석 흐름으로 정리합니다.
핵심 요약
- DuckDB는 SQL과 Python 생태계를 한 프로세스에서 연결해 Pandas·Polars·Arrow 객체를 table처럼 질의하고 결과를 DataFrame/Arrow로 돌려받습니다.
- module-level duckdb.sql()은 전역 in-memory connection을 쓰므로 운영 notebook·batch에서는 명시 connection을 권장합니다.
- replacement scan은 Python 변수명을 SQL에서 그대로 참조하되 read-only이며, DataFrame에 INSERT/UPDATE를 하는 게 아니라 scan만 합니다.
- Arrow Dataset은 projection/filter pushdown이 가능하고 to_arrow_table()로 반환하면 Pandas 변환 비용을 늦추고 Arrow-native 도구와 연결하기 쉽습니다.
- 팀 표준이 SQL이면 DuckDB를 중심에 두고 expression 중심 feature engineering은 Polars를 함께 쓰며, BI는 read-only .duckdb/curated Parquet와 batch refresh로 고정합니다.
Python 2.0 전환 기준
Python client 2.0은 **Python 3.11+**가 필요하며 free-threaded CPython은 지원하지 않습니다. dependency resolver와 연동 패키지의 2.0 지원도 별도로 확인합니다.
| 기존 코드/가정 | 2.0 전환 |
|---|---|
execute(...).fetch_arrow_table()·arrow()·pl() | relation의 sql(...).to_arrow_table()·to_arrow_reader()·pl() 사용 |
relation의 fetchone()·fetchmany()·execute() | cursor식 읽기는 connection의 execute() 결과, relation 고정은 materialize() |
| relation을 여러 번 읽으면 같은 result 재사용 | 매번 쿼리 재실행; streamed batch는 to_arrow_reader() |
| 표현식의 문자열이 열 이름 | col(...)과 lit(...)로 열·값 구분 |
ColumnExpression("t.x") | col("t", "x"); 점이 든 문자열은 하나의 열 이름 |
values({1: "a"}) | 문자열 아닌 key는 MAP; STRUCT가 필요하면 문자열 key 사용 |
rel.offset 같은 열 접근 | 새 메서드와 충돌하므로 rel["offset"] 사용 |
| 열린 execute 결과를 둔 채 다른 쿼리 | 끝까지 소비하거나 complete()·abort()로 종료 |
| 하나의 connection을 여러 thread에서 공유 | thread별 cursor() 사용 |
| 트랜잭션 중 일부 SQL 오류 후 계속 실행 | 실패 후 rollback()하고 재시작 |
execute(...).to_arrow_table()은 행을 복사하며 경고하므로 아래 Arrow 예제는 sql(...) relation을 사용합니다. 결과 shape·NULL·timezone과 기존 BI reader 호환성도 확인합니다.
Python 기본 패턴
import duckdb
con = duckdb.connect("analytics.duckdb")
con.execute("SET threads = 4")
con.execute("SET memory_limit = '8GB'")
df = con.execute("""
SELECT customer_id, sum(amount) AS revenue
FROM read_parquet('curated/orders/*.parquet')
GROUP BY customer_id
""").df()module-level duckdb.sql()은 전역 in-memory connection을 사용합니다. notebook 초반 탐색에는 편하지만, 운영 notebook이나 batch에서는 명시 connection을 권장합니다.
Replacement scan
Pandas DataFrame, Polars DataFrame, Arrow Table은 Python 변수명 그대로 SQL에서 참조할 수 있습니다.
import duckdb
import pandas as pd
customers = pd.DataFrame({"customer_id": ["c1", "c2"], "segment": ["team", "pro"]})
duckdb.sql("""
SELECT segment, count(*) AS customers
FROM customers
GROUP BY ALL
""").df()이 방식은 read-only입니다. DataFrame에 INSERT/UPDATE를 하는 게 아니라, DuckDB가 해당 객체를 scan하는 것뿐입니다.
Arrow Dataset
Arrow Dataset은 directory of Parquet files를 가리키고, DuckDB는 projection/filter를 pushdown할 수 있습니다.
import duckdb
import pyarrow.dataset as ds
dataset = ds.dataset("curated/orders", format="parquet")
con = duckdb.connect()
result = con.sql("""
SELECT order_date, sum(amount) AS revenue
FROM dataset
WHERE order_date >= DATE '2026-01-01'
GROUP BY ALL
""").to_arrow_table()Arrow로 반환하면 Pandas 변환 비용을 늦출 수 있고 다른 Arrow-native 도구와 연결하기 쉽습니다.
Polars와 역할 분담
| 작업 | DuckDB | Polars |
|---|---|---|
| SQL 기반 join/aggregate | 강함 | 가능 |
| lazy dataframe pipeline | 가능하지만 주력 아님 | 강함 |
| Parquet lake 탐색 | 강함 | 강함 |
| BI/SQL 공유 | 강함 | 약함 |
| Python expression 변환 | 약함 | 강함 |
팀 표준이 SQL이면 DuckDB를 중심에 두고 Python expression이 많은 feature engineering은 Polars를 함께 씁니다.
BI 연결
DuckDB는 ODBC/JDBC와 여러 third-party BI 도구에서 사용할 수 있습니다. BI 운영에서는 다음을 고정합니다.
| 항목 | 기준 |
|---|---|
| 데이터 파일 | read-only .duckdb 또는 curated Parquet |
| refresh | batch publish 이후 BI refresh |
| 권한 | OS/cloud storage 권한으로 제한 |
| 쿼리 | BI 사용자가 임의 extension/network access를 열지 못하게 격리 |
2.0 Python appender
Python에서 행을 생산하는 코드에는 appender를 사용할 수 있습니다. 이미 DataFrame/Arrow에 있는 데이터는 SQL scan 또는 con.append() 경로를 우선 비교합니다.
import duckdb
with duckdb.connect() as con:
con.execute("CREATE TABLE measurements (id INTEGER, reading DOUBLE)")
with con.appender("measurements") as appender:
appender.append_row(1, 12.5)
appender.append_rows([(2, 13.0), (3, 11.5)])
print(con.sql("SELECT * FROM measurements ORDER BY id").fetchall())성공한 with 종료 또는 flush()가 버퍼 행을 반영합니다. 예외가 나면 아직 버퍼에 있는 행은 버리지만 이미 commit된 이전 flush까지 취소하지는 않습니다. 전체 작업의 원자성이 필요하면 명시적 transaction을 사용합니다. GC에 flush를 맡기지 말고, connection의 열린 결과는 소비·종료한 뒤 flush합니다. Python Appender.