서론: 데이터가 커지면, 도구도 바뀌어야 합니다
이 시리즈에서 Pandas 3.0, Polars, NumPy 2.4, scikit-learn 1.8, 그리고 데이터 시각화까지 살펴봤습니다. 그런데 한 가지 빠진 퍼즐 조각이 있어요. CSV 파일이 5GB를 넘기는 순간, Pandas의 read_csv()가 메모리 에러를 뱉기 시작하면 어떻게 해야 할까요?
솔직히 말하면, 저도 처음엔 chunking이나 Dask로 어떻게든 해보려고 발버둥 쳤습니다. 그런데 결국 더 근본적인 해결책이 필요하더라고요.
바로 이 지점에서 DuckDB가 등장합니다. 2026년 3월 9일에 릴리스된 DuckDB 1.5.0 "Variegata"는 Python 프로세스 안에서 돌아가는 분석용 SQL 엔진입니다. 서버 설치도, 설정 파일도 필요 없습니다. pip install duckdb 한 줄이면 끝이에요.
이 가이드에서는 DuckDB 1.5의 핵심 기능부터 Pandas/Polars와의 연동, 실전 데이터 분석 워크플로우까지 코드와 함께 하나하나 짚어보겠습니다.
DuckDB란 무엇인가?
DuckDB는 한마디로 "분석계의 SQLite"입니다. SQLite가 트랜잭션(OLTP) 처리에 특화된 임베디드 데이터베이스라면, DuckDB는 분석(OLAP) 워크로드에 최적화된 임베디드 데이터베이스예요.
핵심 아키텍처 특징
- 컬럼 지향(Columnar) 스토리지: 행이 아닌 열 단위로 데이터를 저장합니다. 50개 컬럼 중 3개만 필요한 쿼리라면, 나머지 47개는 아예 읽지 않아요. 이게 생각보다 엄청난 차이를 만듭니다.
- 벡터화 실행 엔진: 한 번에 1,024~2,048개의 값을 묶어서 처리하며, CPU의 SIMD 명령어와 캐시 라인을 최대한 활용합니다.
- 멀티스레드 병렬 처리: 복잡한 쿼리를 자동으로 병렬화합니다. Pandas의 단일 스레드 처리와는 차원이 다르죠.
- 메모리 초과 데이터 처리: RAM보다 큰 데이터도 디스크 스필링을 통해 처리할 수 있습니다.
- 제로 의존성: 외부 서버나 프로세스가 필요 없습니다. Python 프로세스 안에서 바로 실행돼요.
설치 및 첫 번째 쿼리
설치
DuckDB는 Python 3.9 이상을 지원합니다. pip이나 conda 중 편한 걸로 설치하면 됩니다.
# pip으로 설치
pip install duckdb
# conda로 설치
conda install python-duckdb -c conda-forge
# 버전 확인
python -c "import duckdb; print(duckdb.__version__)"
# 1.5.0
첫 번째 쿼리 실행
설치가 끝나면 바로 SQL 쿼리를 실행할 수 있습니다. 별도의 데이터베이스 생성이나 연결 설정 같은 건 필요 없어요.
import duckdb
# 가장 기본적인 쿼리
result = duckdb.sql("SELECT 42 AS answer")
result.show()
# ┌────────┐
# │ answer │
# │ int32 │
# ├────────┤
# │ 42 │
# └────────┘
네, 이게 전부입니다. import하고 바로 SQL 쓰면 돼요.
인메모리 vs 영구 저장
DuckDB는 두 가지 모드로 동작합니다. 상황에 따라 골라 쓰면 됩니다.
import duckdb
# 인메모리 모드 (기본값) — 세션 종료 시 데이터 소멸
con = duckdb.connect()
# 영구 저장 모드 — 파일에 데이터 유지
con = duckdb.connect("my_analytics.db")
# 설정 옵션과 함께 연결
con = duckdb.connect(config={
"threads": 4,
"memory_limit": "8GB"
})
DuckDB 1.5.0 "Variegata" 핵심 신기능
2026년 3월 9일에 릴리스된 DuckDB 1.5.0에는 꽤 흥미로운 기능들이 추가됐습니다. 핵심만 빠르게 짚어볼게요.
1. VARIANT 타입
JSON 타입은 내부적으로 텍스트로 저장되기 때문에 파싱 오버헤드가 큽니다. 새로운 VARIANT 타입은 타입 정보를 포함한 바이너리 데이터로 저장해서 압축률과 쿼리 성능이 크게 좋아졌어요.
import duckdb
con = duckdb.connect()
# VARIANT 타입 컬럼 생성
con.sql("""
CREATE TABLE events (
id INTEGER,
payload VARIANT
)
""")
# 서로 다른 타입의 데이터를 하나의 컬럼에 저장
con.sql("""
INSERT INTO events VALUES
(1, 'hello'::VARIANT),
(2, 42::VARIANT),
(3, [1, 2, 3]::VARIANT)
""")
# 각 행의 실제 타입 확인
con.sql("SELECT id, variant_typeof(payload) FROM events").show()
# ┌───────┬──────────────────────────┐
# │ id │ variant_typeof(payload) │
# ├───────┼──────────────────────────┤
# │ 1 │ VARCHAR │
# │ 2 │ INTEGER │
# │ 3 │ INTEGER[] │
# └───────┘──────────────────────────┘
한 컬럼에 문자열, 정수, 배열이 공존하는데도 각각의 타입을 정확하게 추적합니다. 반정형 데이터를 다룰 때 정말 편하죠.
2. 내장 GEOMETRY 타입
공간 데이터 처리를 위한 GEOMETRY 타입이 DuckDB 코어에 내장됐습니다. 예전에는 spatial 확장을 따로 설치해야 했는데, 이제 기본으로 들어 있어요.
- WKB(Well-Known Binary) 저장으로 약 3배 크기 절감
- 행 그룹 통계(바운딩 박스) 기반 공간 필터링
- CRS(좌표 참조 시스템) 파라미터 지원
3. read_duckdb 함수
다른 DuckDB 데이터베이스 파일을 ATTACH 없이 직접 읽을 수 있게 됐습니다. 글로빙도 지원되니까 여러 파일을 한 번에 처리하기도 편해요.
import duckdb
# 여러 DuckDB 파일을 한 번에 읽기
result = duckdb.sql("""
SELECT min(value), max(value), count(*)
FROM read_duckdb('analytics_*.db')
""")
result.show()
4. 비차단 체크포인팅
체크포인트 중에도 읽기/쓰기가 가능해졌습니다. TPC-H SF100 벤치마크에서 처리량이 약 17% 향상됐다고 하네요. 실서비스에서 체크포인트 때문에 잠깐 멈추던 문제가 해결된 셈입니다.
5. PEG 파서 (실험적)
새로운 PEG 파서는 더 나은 에러 메시지와 TAB 자동완성을 제공합니다. DuckDB 2.0에서 기본 파서가 될 예정이에요.
# PEG 파서 활성화
duckdb.sql("CALL enable_peg_parser()")
파일을 직접 쿼리하기: CSV, Parquet, JSON
DuckDB의 가장 강력한 기능을 하나만 꼽으라면, 저는 이걸 고르겠습니다. 파일을 직접 SQL로 쿼리할 수 있다는 점이에요. 데이터를 먼저 메모리에 올려야 하는 Pandas와 달리, DuckDB는 필요한 부분만 읽습니다.
CSV 파일 쿼리
import duckdb
# CSV 파일을 직접 SQL로 쿼리 — 스키마 자동 감지
result = duckdb.sql("""
SELECT
region,
COUNT(*) AS order_count,
ROUND(AVG(amount), 2) AS avg_amount
FROM 'sales_data.csv'
GROUP BY region
ORDER BY order_count DESC
""")
result.show()
# 여러 CSV 파일을 한 번에 읽기 (글로빙)
result = duckdb.sql("""
SELECT * FROM 'logs/2026-03-*.csv'
WHERE status_code >= 400
""")
파일 경로를 테이블명처럼 넣으면 됩니다. 처음 보면 좀 이상한데, 한 번 써보면 "왜 진작 이렇게 안 했지?" 하는 생각이 들어요.
Parquet 파일 쿼리
Parquet은 컬럼 지향 포맷이라 DuckDB와 궁합이 특히 좋습니다. 필요한 컬럼만 읽고, WHERE 조건에 맞지 않는 행 그룹은 아예 건너뛰어요.
import duckdb
# Parquet 파일 직접 쿼리
result = duckdb.sql("""
SELECT
product_category,
SUM(revenue) AS total_revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM 'warehouse/transactions.parquet'
WHERE transaction_date >= '2026-01-01'
GROUP BY product_category
ORDER BY total_revenue DESC
LIMIT 10
""")
result.show()
# 여러 Parquet 파일을 한 번에 쿼리
result = duckdb.sql("""
SELECT * FROM read_parquet([
'data/q1.parquet',
'data/q2.parquet',
'data/q3.parquet'
])
""")
JSON 파일 쿼리
import duckdb
# JSON 파일 직접 쿼리
result = duckdb.sql("""
SELECT
user_id,
event_type,
timestamp
FROM 'events.json'
WHERE event_type = 'purchase'
ORDER BY timestamp DESC
LIMIT 20
""")
result.show()
CSV를 Parquet으로 변환
대용량 CSV 파일을 Parquet으로 변환하면 저장 공간과 쿼리 속도 모두 크게 개선됩니다. 솔직히 이건 안 할 이유가 없어요.
import duckdb
# CSV → Parquet 변환
duckdb.sql("""
COPY (SELECT * FROM 'raw_data.csv')
TO 'optimized_data.parquet' (FORMAT 'parquet', COMPRESSION 'zstd')
""")
# 변환 결과 확인
duckdb.sql("SELECT COUNT(*) FROM 'optimized_data.parquet'").show()
Pandas · Polars와 함께 사용하기
여기서 중요한 포인트 하나. DuckDB는 Pandas나 Polars의 대체재가 아닙니다. 강력한 보완재예요. 각 도구의 장점을 살린 하이브리드 워크플로우가 2026년 현재 가장 효율적인 접근법이라고 생각합니다.
Pandas DataFrame을 SQL로 쿼리하기
DuckDB는 Pandas DataFrame의 변수명을 그대로 테이블명으로 사용할 수 있습니다. 데이터 복사 없이 제로 카피로 동작하는데, 이 부분이 정말 인상적이에요.
import duckdb
import pandas as pd
# Pandas DataFrame 생성
sales_df = pd.DataFrame({
"product": ["노트북", "키보드", "모니터", "마우스", "노트북"],
"region": ["서울", "부산", "서울", "대전", "부산"],
"amount": [1500000, 89000, 450000, 35000, 1800000],
"quantity": [1, 3, 2, 5, 1]
})
# DataFrame을 SQL로 쿼리 — 변수명이 곧 테이블명!
result = duckdb.sql("""
SELECT
region,
SUM(amount * quantity) AS total_sales,
COUNT(*) AS transaction_count
FROM sales_df
GROUP BY region
ORDER BY total_sales DESC
""")
# 결과를 다시 Pandas DataFrame으로 변환
result_df = result.df()
print(result_df)
Polars DataFrame과 연동
import duckdb
import polars as pl
# Polars DataFrame 생성
orders_pl = pl.DataFrame({
"order_id": [1, 2, 3, 4, 5],
"customer": ["Alice", "Bob", "Alice", "Charlie", "Bob"],
"total": [100.0, 250.0, 75.0, 300.0, 180.0]
})
# Polars DataFrame도 직접 SQL로 쿼리 가능
result = duckdb.sql("""
SELECT
customer,
SUM(total) AS lifetime_value,
COUNT(*) AS order_count
FROM orders_pl
GROUP BY customer
ORDER BY lifetime_value DESC
""")
# 결과를 Polars DataFrame으로 변환
result_pl = result.pl()
print(result_pl)
결과를 다양한 포맷으로 변환
DuckDB 쿼리 결과를 원하는 포맷으로 바로 변환할 수 있습니다. 이게 얼마나 편한지는 직접 써봐야 알아요.
import duckdb
query = duckdb.sql("SELECT * FROM 'data.parquet' LIMIT 100")
# 다양한 출력 포맷
pandas_df = query.df() # Pandas DataFrame
polars_df = query.pl() # Polars DataFrame
arrow_tbl = query.arrow() # PyArrow Table
numpy_arr = query.fetchnumpy() # NumPy 배열 딕셔너리
python_lst = query.fetchall() # Python 리스트
# 파일로 직접 저장
query.write_parquet("output.parquet")
query.write_csv("output.csv")
실전 예제: 대용량 데이터 분석 워크플로우
자, 이제 실전입니다. 실제 업무에서 자주 만나는 시나리오를 DuckDB로 어떻게 해결하는지 살펴볼게요.
예제 1: 수억 행 로그 데이터 분석
웹 서버 로그 파일이 여러 개의 Parquet 파일로 저장되어 있다고 가정하겠습니다.
import duckdb
con = duckdb.connect()
# 여러 Parquet 파일에서 일별 트래픽 분석
daily_traffic = con.sql("""
SELECT
DATE_TRUNC('day', timestamp) AS date,
COUNT(*) AS total_requests,
COUNT(DISTINCT user_id) AS unique_users,
ROUND(AVG(response_time_ms), 2) AS avg_response_ms,
SUM(CASE WHEN status_code >= 500 THEN 1 ELSE 0 END) AS server_errors
FROM 'logs/2026-*.parquet'
WHERE timestamp >= '2026-01-01'
GROUP BY date
ORDER BY date
""")
# 결과를 Pandas로 변환하여 시각화
import matplotlib.pyplot as plt
df = daily_traffic.df()
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
ax1.plot(df["date"], df["unique_users"])
ax1.set_title("일별 순 방문자 수")
ax2.bar(df["date"], df["server_errors"], color="red", alpha=0.7)
ax2.set_title("일별 서버 에러 수")
plt.tight_layout()
plt.savefig("traffic_analysis.png")
예제 2: 윈도우 함수로 고급 분석
SQL에 익숙하다면 윈도우 함수의 강력함을 잘 아실 거예요. Pandas의 groupby().transform()으로 힘겹게 작성하던 로직이 SQL 한 방으로 깔끔하게 해결됩니다.
import duckdb
# 고객별 구매 패턴 분석 — 윈도우 함수 활용
result = duckdb.sql("""
WITH customer_orders AS (
SELECT
customer_id,
order_date,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id ORDER BY order_date
) AS order_sequence,
LAG(order_date) OVER (
PARTITION BY customer_id ORDER BY order_date
) AS prev_order_date,
SUM(amount) OVER (
PARTITION BY customer_id ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_amount
FROM 'orders.parquet'
)
SELECT
customer_id,
order_sequence,
amount,
cumulative_amount,
DATEDIFF('day', prev_order_date, order_date) AS days_since_last_order
FROM customer_orders
WHERE order_sequence <= 10
ORDER BY customer_id, order_sequence
""")
result.show()
예제 3: S3에서 원격 데이터 쿼리
DuckDB는 AWS S3, Azure Blob Storage 등 클라우드 스토리지의 파일도 직접 쿼리할 수 있습니다. 데이터를 로컬에 다운로드할 필요가 없어요.
import duckdb
con = duckdb.connect()
# httpfs 확장 로드
con.install_extension("httpfs")
con.load_extension("httpfs")
# AWS 자격 증명 설정
con.sql("""
SET s3_region = 'ap-northeast-2';
SET s3_access_key_id = 'YOUR_ACCESS_KEY';
SET s3_secret_access_key = 'YOUR_SECRET_KEY';
""")
# S3의 Parquet 파일 직접 쿼리
result = con.sql("""
SELECT
category,
COUNT(*) AS count,
ROUND(AVG(price), 2) AS avg_price
FROM 's3://my-data-lake/products/*.parquet'
GROUP BY category
ORDER BY count DESC
""")
result.show()
성능 비교: DuckDB vs Pandas
자, 여기가 많은 분들이 가장 궁금해하는 부분이죠. 실제 벤치마크에서 DuckDB는 Pandas 대비 어떤 성능을 보여줄까요?
벤치마크: 1,000만 행 집계 쿼리
import duckdb
import pandas as pd
import time
# === Pandas 방식 ===
start = time.time()
df = pd.read_csv("large_dataset.csv") # 10M rows
result_pandas = (
df.groupby(["region", "category"])
.agg({"revenue": "sum", "order_id": "count"})
.sort_values("revenue", ascending=False)
.head(20)
)
pandas_time = time.time() - start
print(f"Pandas: {pandas_time:.2f}초")
# === DuckDB 방식 ===
start = time.time()
result_duckdb = duckdb.sql("""
SELECT
region,
category,
SUM(revenue) AS revenue,
COUNT(order_id) AS order_count
FROM 'large_dataset.csv'
GROUP BY region, category
ORDER BY revenue DESC
LIMIT 20
""").df()
duckdb_time = time.time() - start
print(f"DuckDB: {duckdb_time:.2f}초")
print(f"속도 차이: DuckDB가 {pandas_time/duckdb_time:.1f}배 빠름")
일반적인 벤치마크 결과는 다음과 같습니다.
| 작업 | Pandas | DuckDB | 속도 차이 |
|---|---|---|---|
| CSV 읽기 + 집계 (10M행) | ~45초 | ~3초 | ~15배 |
| Parquet 집계 (50M행) | ~30초 | ~2초 | ~15배 |
| 다중 테이블 JOIN (5M+5M행) | ~25초 | ~1.5초 | ~17배 |
| 윈도우 함수 (10M행) | ~60초 | ~4초 | ~15배 |
숫자가 좀 충격적이죠? DuckDB가 이렇게 빠른 이유는 명확합니다. Pandas가 모든 데이터를 메모리에 올린 뒤 단일 스레드로 처리하는 반면, DuckDB는 필터 푸시다운(조건에 맞지 않는 데이터는 읽지도 않음), 컬럼 프루닝(필요한 컬럼만 읽음), 멀티스레드 병렬 처리를 자동으로 적용합니다.
언제 어떤 도구를 써야 할까?
DuckDB, Pandas, Polars — 세 도구 모두 훌륭하지만, 각자 빛나는 영역이 다릅니다. 아래 표를 참고해서 상황에 맞는 도구를 고르세요.
| 상황 | 추천 도구 | 이유 |
|---|---|---|
| 빠른 탐색적 데이터 분석 (EDA) | Pandas | 생태계 최고, ML 라이브러리와 완벽한 호환 |
| SQL로 대용량 파일 직접 쿼리 | DuckDB | Parquet/CSV를 메모리 로드 없이 처리 |
| 고속 DataFrame 변환 | Polars | Rust 기반 멀티스레드, 지연 평가 최적화 |
| 복잡한 JOIN + 집계 | DuckDB | SQL 쿼리 옵티마이저가 자동으로 최적 실행 계획 생성 |
| ML 모델 학습 전처리 | Pandas + scikit-learn | scikit-learn Pipeline과 직접 연동 |
| 100GB+ 데이터 처리 | DuckDB | 디스크 스필링으로 RAM 초과 데이터 처리 가능 |
| 실시간 시각화 | Pandas + Matplotlib | 가장 성숙한 시각화 생태계 |
2026년 권장 하이브리드 워크플로우
제가 실무에서 가장 많이 쓰는 패턴은 이겁니다. 세 도구를 적재적소에 배치하는 거죠.
import duckdb
import polars as pl
import pandas as pd
# 1단계: DuckDB로 대용량 데이터 초기 처리 (SQL의 강점)
heavy_query = duckdb.sql("""
SELECT
customer_id,
DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS monthly_spend,
COUNT(*) AS order_count
FROM 's3://data-lake/orders/*.parquet'
WHERE order_date >= '2025-01-01'
GROUP BY customer_id, month
""")
# 2단계: Polars로 고속 변환 (DataFrame 변환의 강점)
pl_df = heavy_query.pl()
pl_df = pl_df.with_columns([
(pl.col("monthly_spend") / pl.col("order_count")).alias("avg_order_value"),
pl.col("monthly_spend").rank().alias("spend_rank")
])
# 3단계: Pandas로 ML/시각화 연동 (생태계의 강점)
pd_df = pl_df.to_pandas()
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
pd_df[["monthly_spend_scaled"]] = scaler.fit_transform(
pd_df[["monthly_spend"]]
)
실전 팁 모음
1. 메모리 제한 설정
공유 서버에서 작업한다면 메모리 제한을 꼭 설정하세요. 안 그러면 다른 사람 작업에 영향을 줄 수 있습니다.
import duckdb
# 메모리 사용량 제한
con = duckdb.connect(config={"memory_limit": "4GB", "threads": 4})
# 또는 런타임에 설정 변경
con.sql("SET memory_limit = '4GB'")
2. 스레드 안전성 주의
duckdb.sql()은 전역 연결을 사용하므로 멀티스레딩 환경에서는 반드시 별도의 연결을 생성해야 합니다. 이거 모르고 쓰면 정말 찾기 어려운 버그가 생겨요.
import duckdb
from concurrent.futures import ThreadPoolExecutor
def analyze_partition(partition_file):
# 스레드마다 별도 연결 생성!
con = duckdb.connect()
result = con.sql(f"""
SELECT COUNT(*), AVG(value)
FROM '{partition_file}'
""").fetchone()
return result
files = ["part_01.parquet", "part_02.parquet", "part_03.parquet"]
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(analyze_partition, files))
3. 확장(Extension) 활용
import duckdb
con = duckdb.connect()
# httpfs — S3/HTTP 원격 파일 읽기
con.install_extension("httpfs")
con.load_extension("httpfs")
# 커뮤니티 확장 설치
con.install_extension("h3", repository="community")
con.load_extension("h3")
4. 람다 구문 변경 주의
DuckDB 1.5부터 화살표 구문(x -> x + 1)에 사용 중단 경고가 뜹니다. 2.0에서는 완전히 없어질 예정이니까, 지금부터 Python 스타일 구문을 쓰는 게 좋습니다.
import duckdb
# 이전 방식 (deprecated, 경고 발생)
# duckdb.sql("SELECT list_transform([1,2,3], x -> x * 2)")
# 새로운 방식 (Python 스타일)
duckdb.sql("SELECT list_transform([1,2,3], lambda x: x * 2)").show()
자주 묻는 질문 (FAQ)
DuckDB는 Pandas를 완전히 대체할 수 있나요?
솔직하게 말하면, 아닙니다. DuckDB는 SQL 기반 분석과 대용량 데이터 처리에 최적화되어 있지만, Pandas가 가진 방대한 생태계(scikit-learn, matplotlib, statsmodels 등과의 직접 연동)를 대체하지는 않습니다. DuckDB로 무거운 전처리와 집계를 수행한 뒤, 결과를 Pandas DataFrame으로 변환해서 ML이나 시각화에 활용하는 하이브리드 전략이 가장 효과적이에요.
DuckDB와 SQLite의 차이점은 무엇인가요?
둘 다 서버 없이 프로세스 내에서 실행되는 임베디드 데이터베이스라는 공통점이 있습니다. 하지만 SQLite는 행 지향 저장소로 트랜잭션 처리(OLTP)에 최적화된 반면, DuckDB는 컬럼 지향 저장소로 분석 쿼리(OLAP)에 특화되어 있어요. 대량 데이터 집계, GROUP BY, JOIN 같은 분석 작업에서 DuckDB가 압도적으로 빠릅니다.
DuckDB는 얼마나 큰 데이터를 처리할 수 있나요?
RAM보다 큰 데이터도 처리할 수 있습니다. 벡터화 실행 엔진이 데이터를 스트리밍 방식으로 처리하며, 필요시 자동으로 디스크에 스필링해요. 실제로 8GB RAM 머신에서 수십 GB의 Parquet 파일을 문제없이 쿼리할 수 있습니다. 다만 페타바이트 급 분산 처리에는 Spark이나 Trino 같은 분산 엔진이 더 적합합니다.
Jupyter Notebook에서 DuckDB를 사용할 수 있나요?
네, 완벽하게 지원됩니다. pip install duckdb로 설치한 뒤 바로 사용할 수 있고, jupysql 확장을 함께 쓰면 %%sql 매직 커맨드로 셀에서 직접 SQL을 실행할 수도 있어요. Jupyter에서 .show()를 호출하면 테이블이 깔끔하게 렌더링됩니다.
DuckDB 1.5에서 DuckDB 2.0으로 업그레이드할 때 주의할 점은?
DuckDB 2.0은 2026년 9월에 릴리스될 예정입니다. 가장 큰 변경 사항은 화살표 람다 구문(x -> x + 1)이 기본적으로 비활성화된다는 점과, PEG 파서가 기본 파서가 된다는 점이에요. 지금부터 Python 스타일 람다 구문(lambda x: x + 1)을 사용하는 습관을 들이면 마이그레이션이 수월합니다. 참고로 DuckDB 1.4.0 LTS는 2026년 9월까지 지원되니까, 안정적인 프로덕션 환경이 필요하다면 LTS 버전도 고려해 보세요.