Python API
Python API
DuckDB의 Python 클라이언트를 쓰려면 먼저 설치부터 해야 해요. 설치 방법은 Python 설치 페이지에서 확인할 수 있는데요, 현재 최신 안정 버전은 1.5.5예요.
설치
DuckDB Python API는 pip으로 간단하게 설치할 수 있어요.
pip install duckdb
자세한 내용은 설치 페이지를 참고하면 되고요, conda를 쓴다면 이렇게도 설치할 수 있어요.
conda install python-duckdb -c conda-forge
참고로 DuckDB는 Python 3.9 이상이 필요해요.
기본 API 사용법
DuckDB에서 SQL 쿼리를 실행하는 가장 간단한 방법은 duckdb.sql 명령을 쓰는 거예요.
import duckdb
duckdb.sql("SELECT 42").show()
이 명령은 Python 모듈 안에 전역으로 저장된 인메모리 데이터베이스를 사용해서 쿼리를 실행해요. 쿼리의 결과는 Relation으로 반환되는데요, Relation은 쿼리를 기호로 표현한 것이에요. 즉 결과를 가져오거나 화면에 출력하라고 요청하기 전까지는 실제로 쿼리가 실행되지 않아요.
Relation은 변수에 저장해 두고 이후 쿼리에서 테이블처럼 참조할 수 있어요. 이렇게 하면 쿼리를 조금씩 쌓아 가며 incrementally 구성할 수 있어요.
import duckdb
r1 = duckdb.sql("SELECT 42 AS i")
duckdb.sql("SELECT i * 2 AS k FROM r1").show()
데이터 입력
DuckDB는 디스크에 있는 형식과 메모리 안의 형식을 아우르는 다양한 포맷의 데이터를 받아들일 수 있어요. 자세한 내용은 데이터 수집 페이지에서 다루는데요, 핵심적인 것만 먼저 볼게요.
import duckdb
duckdb.read_csv("example.csv") # CSV 파일을 Relation으로 읽기
duckdb.read_parquet("example.parquet") # Parquet 파일을 Relation으로 읽기
duckdb.read_json("example.json") # JSON 파일을 Relation으로 읽기
duckdb.sql("SELECT * FROM 'example.csv'") # CSV 파일을 직접 쿼리
duckdb.sql("SELECT * FROM 'example.parquet'") # Parquet 파일을 직접 쿼리
duckdb.sql("SELECT * FROM 'example.json'") # JSON 파일을 직접 쿼리
DataFrames
DuckDB는 Pandas DataFrame, Polars DataFrame, Arrow table을 직접 쿼리할 수 있어요. 다만 이들은 읽기 전용이라는 점을 기억해야 해요. 즉 INSERT나 UPDATE 문으로 이 테이블들을 수정하는 것은 불가능해요.
Pandas
Pandas DataFrame을 직접 쿼리하려면 이렇게 하면 돼요.
import duckdb
import pandas as pd
pandas_df = pd.DataFrame({"a": [42]})
duckdb.sql("SELECT * FROM pandas_df")
┌───────┐
│ a │
│ int64 │
├───────┤
│ 42 │
└───────┘
Polars
Polars DataFrame도 마찬가지로 직접 쿼리할 수 있어요.
import duckdb
import polars as pl
polars_df = pl.DataFrame({"a": [42]})
duckdb.sql("SELECT * FROM polars_df")
┌───────┐
│ a │
│ int64 │
├───────┤
│ 42 │
└───────┘
PyArrow
PyArrow table도 동일하게 쿼리할 수 있어요.
import duckdb
import pyarrow as pa
arrow_table = pa.Table.from_pydict({"a": [42]})
duckdb.sql("SELECT * FROM arrow_table")
┌───────┐
│ a │
│ int64 │
├───────┤
│ 42 │
└───────┘
결과 변환
DuckDB는 쿼리 결과를 여러 포맷으로 효율적으로 변환하는 기능을 지원해요. 자세한 내용은 결과 변환 페이지에 있는데, 자주 쓰는 변환들을 먼저 살펴볼게요.
import duckdb
duckdb.sql("SELECT 42").fetchall() # Python 객체
duckdb.sql("SELECT 42").df() # Pandas DataFrame
duckdb.sql("SELECT 42").pl() # Polars DataFrame
duckdb.sql("SELECT 42").arrow() # Arrow Table
duckdb.sql("SELECT 42").fetchnumpy() # NumPy 배열
데이터를 디스크에 쓰기
DuckDB는 Relation 객체를 여러 포맷으로 디스크에 직접 저장할 수 있어요. 대안으로 COPY 문을 써서 SQL로 데이터를 쓰는 방법도 있어요.
import duckdb
duckdb.sql("SELECT 42").write_parquet("out.parquet") # Parquet 파일로 쓰기
duckdb.sql("SELECT 42").write_csv("out.csv") # CSV 파일로 쓰기
duckdb.sql("COPY (SELECT 42) TO 'out.parquet'") # COPY 문으로 Parquet 파일에 쓰기
연결 옵션
애플리케이션은 duckdb.connect() 메서드로 새로운 DuckDB 연결을 열 수 있어요.
인메모리 데이터베이스 사용하기
duckdb.sql()을 쓸 때 DuckDB는 인메모리 데이터베이스에서 동작하는데요, 이 경우 어떤 테이블도 디스크에 저장되지 않아요. duckdb.connect()를 인자 없이 호출하면 역시 인메모리 데이터베이스를 사용하는 연결이 반환돼요.
import duckdb
con = duckdb.connect()
con.sql("SELECT 42 AS x").show()
영구 저장소
duckdb.connect(dbname)은 영구 데이터베이스에 대한 연결을 만들어요. 그 연결을 통해 쓴 데이터는 모두 저장되고, 같은 파일에 다시 연결하면 다시 불러올 수 있어요. Python뿐 아니라 다른 DuckDB 클라이언트에서도요.
import duckdb
# 'file.db'라는 파일에 대한 연결 만들기
con = duckdb.connect("file.db")
# 테이블을 만들고 데이터를 넣기
con.sql("CREATE TABLE test (i INTEGER)")
con.sql("INSERT INTO test VALUES (42)")
# 테이블 쿼리하기
con.table("test").show()
# 연결을 명시적으로 닫기
con.close()
# 참고: 연결은 범위(scope)를 벗어나면 암시적으로도 닫혀요
연결이 반드시 닫히도록 컨텍스트 매니저를 사용할 수도 있어요.
import duckdb
with duckdb.connect("file.db") as con:
con.sql("CREATE TABLE test (i INTEGER)")
con.sql("INSERT INTO test VALUES (42)")
con.table("test").show()
# 컨텍스트 매니저가 연결을 자동으로 닫아 줘요
설정
duckdb.connect()은 config 딕셔너리를 받아서 설정 옵션을 지정할 수 있어요. 예를 들면 이렇게요.
import duckdb
con = duckdb.connect(config = {'threads': 1})
저장소 버전을 지정하려면 storage_compatibility_version 옵션을 넘기면 돼요.
import duckdb
con = duckdb.connect(config = {'storage_compatibility_version': 'latest'})
연결 객체와 모듈
연결 객체와 duckdb 모듈은 서로 바꿔 쓸 수 있어요. 둘 다 동일한 메서드를 지원하거든요. 단 하나의 차이점은 duckdb 모듈을 쓸 때는 전역 인메모리 데이터베이스가 사용된다는 점이에요.
참고
다른 사람이 쓸 패키지를 만들면서 그 안에서 DuckDB를 사용한다면,
duckdb모듈의 메서드 대신 연결 객체를 만드는 방식을 권장해요.duckdb모듈은 전역 데이터베이스를 공유하는데, 여러 패키지에서 함께 쓰면 디버깅하기 어려운 문제가 생길 수 있기 때문이에요.
병렬 Python 프로그램에서 연결 사용하기
duckdb.sql()과 전역 연결의 스레드 안전성
duckdb.sql()과 duckdb.connect(':default:')는 전역 인메모리 연결 하나를 공유해요. 이 연결은 스레드에 안전하지 않아서, 여러 스레드에서 동시에 쿼리를 실행하면 문제가 생길 수 있어요. DuckDB를 병렬로 돌리려면 각 스레드가 자기만의 연결을 가져야 해요.
def good_use():
con = duckdb.connect()
# 새 연결 사용
con.sql("SELECT 1").fetchall()
반대로 아래 코드들은 전역 연결에 의존하기 때문에 동시성 문제를 일으킬 수 있어요.
def bad_use():
con = duckdb.connect(':default:')
# 전역 연결 사용
return con.sql("SELECT 1").fetchall()
혹은 이렇게요.
def also_bad():
return duckdb.sql("SELECT 1").fetchall()
# 전역 연결 사용
duckdb.sql()을 쓰거나 하나의 연결을 여러 스레드에서 공유하는 것은 피하는 게 좋아요.
cursor()에 대해
DuckDBPyConnection.cursor() 메서드는 같은 연결에 대한 또 다른 핸들을 만들어요. 새 연결을 여는 게 아니라는 점이 중요해요. 그래서 하나의 연결에서 만들어진 모든 cursor는 동시에 쿼리를 실행할 수 없어요.
커뮤니티 확장
커뮤니티 확장을 로드하려면 install_extension 메서드에 repository="community" 인자를 넘기면 돼요.
예를 들어 h3 커뮤니티 확장을 설치하고 로드하는 방법은 이렇습니다.
import duckdb
con = duckdb.connect()
con.install_extension("h3", repository="community")
con.load_extension("h3")
서명되지 않은 확장(Unsigned Extensions)
서명되지 않은 확장을 로드하려면 이렇게 설정하면 돼요.
con = duckdb.connect(config={"allow_unsigned_extensions": "true"})
경고
서명되지 않은 확장은 신뢰할 수 있는 소스에서만 로드하세요. HTTP를 통한 서명되지 않은 확장 로드는 피해야 하고요, DuckDB를 안전하게 구성하는 방법은 Securing DuckDB 페이지를 참고하세요.