DuckDB ใน Python 2026: คู่มือฉบับสมบูรณ์ พร้อมตัวอย่างใช้งานจริง

DuckDB คือฐานข้อมูล OLAP แบบ in-process สำหรับ Python ที่เร็วกว่า pandas หลายเท่าและรัน SQL บน DataFrame, Parquet, CSV ได้โดยตรง บทความนี้สอนติดตั้ง รัน query แรก เทียบกับ pandas/SQLite และใช้งานจริงกับ FastAPI

DuckDB Python: คู่มือฉบับสมบูรณ์ 2026

อัปเดตล่าสุด: 22 มิถุนายน 2026

DuckDB คือฐานข้อมูลเชิงวิเคราะห์ (OLAP) แบบ in-process ที่รันอยู่ในกระบวนการ Python เดียวกับโค้ดของคุณ คล้าย SQLite แต่ออกแบบมาเพื่อ analytics โดยเฉพาะ มันใช้สถาปัตยกรรมแบบ columnar storage และ vectorized execution ทำให้ query บน DataFrame, ไฟล์ Parquet หรือ CSV ขนาดหลาย GB ได้เร็วกว่า pandas หลายเท่าโดยไม่ต้องตั้งเซิร์ฟเวอร์ ผมใช้ DuckDB เป็นเครื่องมือหลักในการเชื่อม FastAPI endpoint เข้ากับงานวิเคราะห์ข้อมูลขนาดใหญ่มาตั้งแต่ปี 2024 ในบทความนี้ผมจะพาคุณติดตั้ง รัน query แรก เทียบกับ pandas/SQLite และดู pattern ที่ผมใช้จริงในโปรดักชัน

  • DuckDB เวอร์ชัน 1.x มี on-disk storage format ที่ stable แล้ว ไฟล์ .duckdb ของคุณจะอ่านได้ตลอดในเวอร์ชันถัดไป
  • query บน pandas DataFrame ได้โดยตรงด้วย duckdb.sql("SELECT * FROM df") ไม่ต้อง copy ข้อมูล (zero-copy ผ่าน Apache Arrow)
  • อ่าน Parquet/CSV หลายไฟล์พร้อมกันด้วย glob pattern เช่น read_parquet('data/*.parquet') โดยไม่ต้อง load เข้า memory ทั้งหมด
  • เร็วกว่า pandas สำหรับ aggregate และ join ใน dataset ขนาดใหญ่ มักจะ 5–50 เท่าในเบนช์มาร์กของทีม DuckDB
  • ใช้ในโปรเซสเดียวกับ FastAPI/Jupyter ได้เลย ไม่มี network round-trip และไม่ต้อง deploy แยก
  • รองรับ extensions เช่น httpfs (อ่าน S3/HTTP โดยตรง), spatial, full-text search และ JSON

DuckDB คืออะไรและทำไมถึงน่าใช้

DuckDB เป็นฐานข้อมูล analytical แบบ embedded ที่พัฒนาโดยทีมวิจัยจาก CWI (Centrum Wiskunde & Informatica) ในเนเธอร์แลนด์ จุดขายหลักของมันคือ "SQLite for analytics" หมายความว่ามันรันใน-process เหมือน SQLite (ไม่มี server, ไม่มี config) แต่ทุกอย่างถูกออกแบบใหม่เพื่อ workload แบบ OLAP คือ aggregate, join, scan ข้อมูลปริมาณมหาศาล ไม่ใช่ insert ทีละแถวแบบ OLTP

สิ่งที่ทำให้ DuckDB เร็วคือสามอย่าง ได้แก่ columnar storage (เก็บข้อมูลเป็นคอลัมน์ทำให้ scan เฉพาะคอลัมน์ที่ใช้), vectorized execution engine (ประมวลผลทีละ batch หลายพันแถว ใช้ CPU cache ได้ดี) และ cost-based query optimizer ที่จัดลำดับ join และ filter อย่างชาญฉลาด ผลคือใน benchmark TPC-H ที่ทีม DuckDB เผยแพร่ DuckDB เร็วกว่า pandas ในงาน aggregate ขนาดใหญ่หลายสิบเท่า รายละเอียดดูที่ หน้า Why DuckDB ของทีมโครงการ

สำหรับนักพัฒนาที่มาจากฝั่ง API อย่างผม จุดที่น่าสนใจที่สุดคือมัน "หายไป" ใน deployment. ไม่ต้องตั้ง Postgres, ไม่ต้องเชื่อม BigQuery, ไม่ต้องจัดการ network แค่ pip install duckdb แล้วเขียน SQL ได้เลย เหมาะมากกับ analytics dashboard, batch ETL, หรือ data API ที่ต้องการความเร็วแต่ไม่ต้องการ infrastructure ที่ซับซ้อน

ติดตั้ง DuckDB ใน Python

การติดตั้งทำได้ผ่าน pip มาตรฐาน. DuckDB เผยแพร่ wheel สำหรับทุก platform หลัก (Linux, macOS, Windows) และทุก Python เวอร์ชันที่ยัง support ปัจจุบัน Python 3.9 ถึง 3.13 รวมถึง uv ก็ติดตั้งได้ตามปกติ

# ติดตั้งเวอร์ชันเสถียรล่าสุด (1.x storage format stable แล้ว)
pip install duckdb

# ถ้าใช้ uv (เร็วกว่ามาก)
uv add duckdb

# ตรวจสอบเวอร์ชัน
python -c "import duckdb; print(duckdb.__version__)"

หลังติดตั้งเสร็จ ผมแนะนำให้ตรวจว่า extension system ทำงานด้วย เพราะถ้าจะอ่านไฟล์จาก S3 หรือ HTTPS ในขั้นถัดไป จะต้องใช้ extension httpfs ซึ่ง DuckDB จะดาวน์โหลดอัตโนมัติเมื่อเรียก INSTALL httpfs; LOAD httpfs;

Query แรกของคุณ: in-memory และ persistent

DuckDB มีสองโหมดหลัก คือ in-memory (เก็บข้อมูลใน RAM เท่านั้น) และ persistent (บันทึกลงไฟล์ .duckdb). ส่วนตัวผมเริ่มทุกโปรเจกต์ด้วย in-memory เพื่อ prototype จากนั้นค่อยย้ายเป็นไฟล์เมื่อโครงสร้างนิ่งแล้ว เคยพยายามใช้ไฟล์ตั้งแต่ต้นรอบหนึ่ง ผลคือเสียเวลาแก้ schema migration ทุกครั้งที่เปลี่ยนใจ

import duckdb

# โหมด in-memory ข้อมูลหายเมื่อจบ session
con = duckdb.connect()  # หรือ duckdb.connect(":memory:")

# สร้างตารางและใส่ข้อมูล
con.execute("""
    CREATE TABLE sales (
        order_id INTEGER,
        customer VARCHAR,
        amount DECIMAL(10, 2),
        order_date DATE
    )
""")
con.execute("""
    INSERT INTO sales VALUES
    (1, 'Somchai', 1250.00, '2026-06-01'),
    (2, 'Niran', 890.50, '2026-06-02'),
    (3, 'Somchai', 2100.00, '2026-06-15')
""")

# Query และดึงผลลัพธ์
result = con.execute("""
    SELECT customer, SUM(amount) AS total
    FROM sales
    GROUP BY customer
    ORDER BY total DESC
""").fetchall()
print(result)
# [('Somchai', Decimal('3350.00')), ('Niran', Decimal('890.50'))]

สำหรับ persistent database เปลี่ยน argument เป็น path ของไฟล์. DuckDB จะสร้างไฟล์ใหม่ถ้ายังไม่มี หรือเปิดอันเดิมถ้ามีอยู่แล้ว

# บันทึกลงไฟล์ ใช้ซ้ำได้ในรันถัดไป
con = duckdb.connect("analytics.duckdb")

# Connection แบบ read-only สำหรับ multi-process read
con = duckdb.connect("analytics.duckdb", read_only=True)

วิธีดึงผลลัพธ์ยืดหยุ่นมาก. นอกจาก .fetchall() ยังมี .df() (เป็น pandas), .pl() (เป็น Polars), .arrow() (เป็น PyArrow Table) และ .fetchnumpy() เลือกใช้ตาม downstream pipeline ของคุณได้ทันที

Query pandas DataFrame ด้วย DuckDB

ฟีเจอร์ที่ผมว่าเปลี่ยน workflow ได้มากที่สุดคือ DuckDB query DataFrame ที่อยู่ใน Python scope ของคุณได้โดยตรง โดยไม่ต้อง register หรือ load เข้า table ก่อน เพราะมันใช้ Apache Arrow เป็นชั้นกลางและทำ zero-copy ให้โดยอัตโนมัติ

import duckdb
import pandas as pd

# สมมุติเรามี DataFrame ปกติ
df = pd.DataFrame({
    "product": ["A", "B", "A", "C", "B"],
    "region": ["TH", "TH", "VN", "TH", "VN"],
    "revenue": [100, 200, 150, 80, 220],
})

# Query DataFrame โดยตรง อ้างชื่อตัวแปร df ใน FROM ได้เลย
result = duckdb.sql("""
    SELECT region,
           product,
           SUM(revenue) AS total_revenue,
           COUNT(*) AS orders
    FROM df
    GROUP BY region, product
    ORDER BY total_revenue DESC
""").df()

print(result)

ในเคสที่คุณมี DataFrame ขนาด 5–10 ล้านแถวและต้อง groupby + join เทียบกับเทคนิคในบทความเร่งความเร็ว pandas DataFrame ที่ผมเขียนไว้ก่อนหน้า DuckDB มักให้ผลเร็วกว่าโดยไม่ต้องเปลี่ยน DataFrame เลย เพราะ engine ของมันคำนวณคู่ขนานได้และใช้ memory อย่างมีประสิทธิภาพ

อ่าน Parquet, CSV และ JSON โดยตรง

หนึ่งใน superpower ของ DuckDB คืออ่านไฟล์ Parquet/CSV/JSON ขนาดใหญ่ที่อาจ "ใหญ่กว่า RAM" ได้ เพราะ engine ทำ streaming และ predicate pushdown ให้อัตโนมัติ ไม่จำเป็นต้อง load ทั้งไฟล์เข้า memory ก่อน query

# อ่านไฟล์ Parquet ไฟล์เดียว
duckdb.sql("SELECT COUNT(*) FROM 'data/transactions.parquet'").show()

# อ่านหลายไฟล์ในโฟลเดอร์ด้วย glob pattern
duckdb.sql("""
    SELECT customer_id, SUM(amount) AS total
    FROM 'data/year=2026/month=*/transactions-*.parquet'
    WHERE category = 'electronics'
    GROUP BY customer_id
    HAVING total > 10000
""").df()

# อ่าน CSV ที่ DuckDB sniffer จะเดา schema ให้
df = duckdb.sql("""
    SELECT * FROM read_csv('large_log.csv',
                            header=true,
                            auto_detect=true)
    WHERE response_time_ms > 500
""").df()

# อ่าน JSON (รวมถึง NDJSON / JSON Lines)
duckdb.sql("""
    SELECT user_id, event_type, count(*)
    FROM read_json('events/*.jsonl')
    GROUP BY 1, 2
""").show()

สำหรับงาน ETL ผมใช้ pattern นี้แทน pandas เกือบทั้งหมดในปี 2026 เพราะมัน scale ขึ้นและลงได้โดยอัตโนมัติ ใน เอกสารทางการของ DuckDB เรื่อง Parquet มีรายละเอียดเพิ่มเติมเรื่อง row group filtering และ projection pushdown ที่ทำให้ scan เร็วขึ้นอีก

DuckDB vs pandas: เมื่อไรควรใช้อะไร

คำถามที่ทุกคนถามผมคือ "แล้วยังต้องใช้ pandas อยู่ไหม?" คำตอบสั้นๆ คือ ใช้ทั้งคู่ แต่เลือกเครื่องมือให้ตรงกับงาน. DuckDB ชนะชัดเจนเรื่อง aggregate และ join บน data ขนาดใหญ่ ส่วน pandas ยังคงเป็นมาตรฐานสำหรับ row-wise manipulation, time-series operations และ ecosystem ของ ML libraries

หัวข้อDuckDBpandas
โมเดลข้อมูลColumnar (Arrow)Row + Column hybrid (NumPy/Arrow)
ขนาดข้อมูลที่เหมาะหลาย GB ถึง TB (larger-than-memory ได้)พอดี RAM (โดยทั่วไป < 5–10 GB)
ความเร็ว aggregate5–50× เร็วกว่า (TPC-H benchmark)มาตรฐาน baseline
ภาษาที่ใช้เขียนSQL (ANSI + DuckDB extensions)Python method chaining
ParallelismMulti-threaded อัตโนมัติSingle-threaded (ต้องใช้ Dask/modin ช่วย)
การใช้ร่วมกันQuery DataFrame ได้โดยตรงรับผลจาก DuckDB ผ่าน .df() ได้
การใช้ memoryStreaming / spill-to-diskโหลดทั้งหมดเข้า RAM

workflow ที่ผมใช้จริงคือใช้ DuckDB ทำ heavy aggregation/join จาก Parquet ก่อน แล้ว .df() ผลลัพธ์ขนาดเล็กลงไปยัง pandas เพื่อทำ feature engineering หรือ visualize ต่อด้วย matplotlib/seaborn. ส่วน Polars vs Pandas ที่ผมเทียบไว้ก่อนหน้า นั้น Polars เป็นทางเลือกที่ดีในระดับ DataFrame แต่ถ้าคุณคิดเป็น SQL อยู่แล้ว DuckDB จะลื่นกว่ามาก

DuckDB กับ SQLite ต่างกันยังไง

ทั้งคู่เป็น embedded database แต่ออกแบบมาเพื่อ workload คนละแบบสิ้นเชิง. SQLite เป็น OLTP database เหมาะกับการ insert/update ทีละแถว เช่น mobile app, web session storage, config — มี row-oriented storage และ B-tree index ที่เหมาะกับ point lookup

DuckDB เป็น OLAP database เหมาะกับการ scan และ aggregate หลายล้านแถว. columnar storage ทำให้ SELECT AVG(price) FROM products ไม่ต้องอ่านทุกคอลัมน์ มันอ่านแค่คอลัมน์ price เท่านั้น ทำให้เร็วกว่า SQLite ในงาน analytics หลายสิบเท่า แต่ถ้างานของคุณเป็น "insert 1000 transactions ต่อวินาที แล้ว lookup ด้วย user_id" SQLite จะยังเหมาะกว่า

กฎที่ผมใช้ตัดสินใจคือ ถ้า query หลักเริ่มต้นด้วย SELECT … GROUP BY ใช้ DuckDB ถ้าหลักๆ เป็น INSERT หรือ SELECT WHERE id = ? ใช้ SQLite ที่จริงในหลายระบบใช้ทั้งคู่ คือ SQLite สำหรับ transactional state แล้ว ETL ออกมาเป็น Parquet ให้ DuckDB อ่านสำหรับ analytics

ใช้ DuckDB กับ FastAPI แบบที่ผมใช้จริง

ส่วนนี้คือเหตุผลที่ผมรัก DuckDB เป็นพิเศษ. มันรวมเข้ากับ FastAPI ได้แบบเรียบไม่มีรอย ไม่มี connection pool ที่ต้องตั้ง ไม่มี async driver ที่ต้องเลือก เพียงเปิด connection ครั้งเดียวตอน startup แล้วใช้ผ่าน app.state

from fastapi import FastAPI, Depends
from contextlib import asynccontextmanager
import duckdb

@asynccontextmanager
async def lifespan(app: FastAPI):
    # เปิด connection ตอน startup, read_only ปลอดภัยกว่าใน API
    app.state.duck = duckdb.connect("analytics.duckdb", read_only=True)
    yield
    app.state.duck.close()

app = FastAPI(lifespan=lifespan)

def get_db(app: FastAPI):
    # DuckDB connection thread-safe, สร้าง cursor ใหม่ต่อ request
    return app.state.duck.cursor()

@app.get("/api/top-products")
def top_products(limit: int = 10, db = Depends(get_db)):
    rows = db.execute("""
        SELECT product_id, SUM(revenue) AS total
        FROM 'data/sales/*.parquet'
        WHERE order_date >= CURRENT_DATE - INTERVAL 30 DAY
        GROUP BY product_id
        ORDER BY total DESC
        LIMIT ?
    """, [limit]).fetchall()
    return [{"product_id": r[0], "total": float(r[1])} for r in rows]

จุดที่ต้องระวังคือ DuckDB connection หลักนั้น thread-safe แต่ "cursor" หนึ่งตัวห้ามใช้พร้อมกันหลายคำสั่ง. วิธีที่ปลอดภัยคือเรียก .cursor() ใหม่ทุก request ตามตัวอย่างข้างบน หรือใช้ connection.duplicate() สำหรับ session แยก. สำหรับ FastAPI ที่รัน async ผมแนะนำให้ห่อ query ที่ใช้เวลานานด้วย run_in_threadpool เพื่อไม่ให้ block event loop. ผมเคยเจอ bug ตัวนี้ตอนชิป endpoint ที่ join 4 ไฟล์ Parquet, latency รวมพุ่งจาก 80ms เป็น 1.2s ทันทีที่ traffic เข้ามาพร้อมกัน

Extensions ที่ควรรู้: httpfs, spatial, fts

DuckDB มี extension system ที่ทำให้ความสามารถขยายได้เกินกว่า SQL พื้นฐาน. extension ที่ผมใช้บ่อยมีสามตัว ติดตั้งด้วย INSTALL ext_name; LOAD ext_name; ทำเพียงครั้งเดียวต่อ database. ดูรายการเต็มได้ที่ DuckDB Extensions documentation

# httpfs อ่านไฟล์จาก S3, HTTPS, GCS โดยตรง
duckdb.sql("INSTALL httpfs; LOAD httpfs;")
duckdb.sql("""
    SELECT *
    FROM 's3://my-bucket/data/2026/*.parquet'
    LIMIT 10
""")

# spatial geometric operations แบบ PostGIS
duckdb.sql("INSTALL spatial; LOAD spatial;")
duckdb.sql("""
    SELECT name, ST_Distance(geom, ST_Point(100.5018, 13.7563)) AS dist_to_bkk
    FROM stations
    ORDER BY dist_to_bkk
    LIMIT 5
""")

# fts full-text search
duckdb.sql("INSTALL fts; LOAD fts;")
duckdb.sql("PRAGMA create_fts_index('docs', 'id', 'content')")
duckdb.sql("""
    SELECT *, fts_main_docs.match_bm25(id, 'python pandas') AS score
    FROM docs WHERE score IS NOT NULL ORDER BY score DESC
""")

นอกจากนี้ยังมีกลุ่ม extension สำหรับ format อื่นๆ เช่น excel (อ่าน .xlsx), iceberg (อ่าน Apache Iceberg tables), delta (อ่าน Delta Lake) ทำให้ DuckDB กลายเป็น "Swiss army knife" สำหรับการอ่านข้อมูลจากแทบทุก source

ข้อควรระวังที่ผมเคยเจอ

ปัญหาที่พบบ่อยที่สุดคือเรื่อง memory limit. DuckDB จะใช้ RAM ทั้งหมดที่มีถ้าจำเป็น แต่ใน container ที่จำกัด memory อยู่แล้ว ควรตั้ง PRAGMA memory_limit='4GB' เพื่อให้ engine spill to disk เมื่อใช้เกิน ไม่อย่างนั้น process อาจถูก OOM killer ฆ่า. ผมเคยโดนเรื่องนี้ตอน deploy ครั้งแรกบน Kubernetes, pod ถูก kill เงียบๆ ทุกตี 2 ที่ ETL job ทำงาน

เรื่องที่สอง, DuckDB ไม่มี user authentication. มันเป็น embedded database ไฟล์ของคุณก็คือ database ทั้งหมด ใครเข้าถึงไฟล์ได้ก็เข้าถึงข้อมูลได้ทั้งหมด ดังนั้นอย่าฝัง credential ใน analytics.duckdb แล้ว expose ผ่าน static file server

เรื่องสุดท้าย, type coercion จาก pandas DataFrame ที่มี object column ที่ไม่ใช่ string ล้วน อาจทำให้ DuckDB อ่านเป็น VARCHAR แล้วต้อง CAST กลับ. ทางออกคือแปลง dtype ให้ชัดเจนใน pandas ก่อน หรือใช้ Arrow DataFrame ตั้งแต่ต้น คล้ายกับเรื่องที่ผมเคยพูดถึงในบทความเรื่องรวม DataFrame ด้วย merge join concat เรื่อง schema ที่ชัดเจนตั้งแต่ต้นช่วยลดบั๊กได้มาก

คำถามที่พบบ่อย

DuckDB เร็วกว่า pandas จริงไหม?

ใช่ในงาน aggregate, join และ scan ข้อมูลขนาดใหญ่ (หลาย GB ขึ้นไป) DuckDB เร็วกว่า pandas ประมาณ 5–50 เท่าใน TPC-H benchmark เพราะใช้ columnar storage, vectorized execution และ multi-threading โดยอัตโนมัติ แต่ pandas ยังเร็วกว่าสำหรับ row-wise operation บนข้อมูลขนาดเล็ก

DuckDB ใช้แทน SQLite ได้ไหม?

ใช้แทนไม่ได้ในทุกกรณี. SQLite เหมาะกับ OLTP (insert/update ทีละแถว, point lookup) ส่วน DuckDB เหมาะกับ OLAP (aggregate, scan ข้อมูลใหญ่) ถ้า workload หลักของคุณเป็น INSERT และ WHERE id = ? ให้ใช้ SQLite ถ้าเป็น SELECT … GROUP BY ให้ใช้ DuckDB

DuckDB ต่างจาก Polars อย่างไร?

ทั้งคู่ใช้ Apache Arrow และ vectorized execution เหมือนกัน แต่ Polars เป็น DataFrame library ที่เขียนแบบ method chaining (คล้าย pandas) ส่วน DuckDB ใช้ SQL เป็น interface หลัก เลือกตามความถนัด. ถ้าทีมคุณคุ้นเคย SQL อยู่แล้วและต้องการอ่าน Parquet/CSV ตรงๆ DuckDB จะเข้าใจง่ายกว่า

DuckDB รองรับ concurrent write ไหม?

รองรับแบบ single-writer/multiple-reader เหมือน SQLite. มี process เดียวที่เปิดแบบ write mode ได้ แต่หลาย process เปิดแบบ read-only พร้อมกันได้ ใน FastAPI ที่ scale แนวนอน ผมจึงใช้ pattern แยกระหว่าง ETL job (เขียน) กับ API (อ่านอย่างเดียวจากไฟล์ที่ ETL สร้าง)

ติดตั้ง DuckDB extension ทุกครั้งหรือไม่?

ไม่ต้อง. INSTALL ติดตั้ง extension ไว้ใน cache ของระบบ (default อยู่ที่ ~/.duckdb/extensions/) ทำเพียงครั้งเดียวต่อเครื่อง แต่ต้องเรียก LOAD ทุกครั้งที่เปิด connection ใหม่เพื่อให้ extension พร้อมใช้งานในเซสชันนั้น

Tomás Oliveira
เกี่ยวกับผู้เขียน Tomás Oliveira

Python backend developer who came to data work via FastAPI. Bridges the messy world between APIs and pipelines.