Python hiện đại 3 — DuckDB: SQL OLAP nhúng

13 thg 7, 2026 3 lượt xem
#sql
#analytics
#olap
#python
#duckdb

Trong Tổng quan Python hiện đại cho Data chúng ta đã điểm qua bộ công cụ dữ liệu thế hệ mới, và bài về Polars đã cho thấy sức mạnh của DataFrame viết bằng Rust. Bài này giới thiệu người bạn đồng hành thứ hai của Polars: DuckDB — một cơ sở dữ liệu phân tích nhúng cho phép bạn viết SQL thuần thẳng trên các file dữ liệu, không cần server, không cần nạp trước.

Nếu bạn quen với SQLite — một CSDL SQL gói gọn trong một thư viện, không cần cài server, chạy ngay trong ứng dụng — thì hãy hình dung DuckDB là "SQLite cho phân tích". Cùng triết lý nhúng, nhưng thay vì tối ưu cho ghi/đọc từng dòng (OLTP) như SQLite, DuckDB được thiết kế cho các truy vấn phân tích quét hàng triệu dòng, tính tổng hợp, join lớn (OLAP).

DuckDB là gì

DuckDB là một CSDL OLAP (Online Analytical Processing — xử lý phân tích, đối lập với OLTP xử lý giao dịch) với bốn đặc điểm cốt lõi:

  • In-process (nhúng, chạy trong tiến trình): không có server riêng, không có cổng mạng, không cần cài đặt phức tạp. DuckDB là một thư viện được nạp thẳng vào tiến trình Python (hay R, Java, Node, C++) của bạn. Cài chỉ một lệnh:
pip install duckdb
  • Lưu trữ cột (columnar): dữ liệu được tổ chức theo cột thay vì theo dòng. Truy vấn phân tích thường chỉ đụng vài cột trong bảng rộng — lưu cột giúp chỉ đọc đúng cột cần, thân thiện với cache CPU và nén tốt hơn nhiều.
  • Vectorized execution: thay vì xử lý từng dòng một (như nhiều CSDL truyền thống), DuckDB xử lý theo từng lô vector khoảng 2048 giá trị mỗi lần. Cách này tận dụng lệnh SIMD của CPU và giảm chi phí điều phối, cho thông lượng cao.
  • Đa lõi (multi-threaded): một truy vấn GROUP BY hay join lớn tự động được chia ra tất cả các CPU core của máy, không cần cấu hình gì.

Điểm chung với Polars rất rõ: cả hai đều columnar, vectorized, đa lõi, và đều dựa nền Apache Arrow. Sự khác biệt nằm ở giao diện: Polars cho bạn expression API kiểu DataFrame, còn DuckDB cho bạn SQL đầy đủ. Trong thực tế người ta dùng cả hai cạnh nhau.

Sức mạnh cốt lõi: SQL thẳng trên file, không cần ETL

Đây là điều làm DuckDB khác biệt và gây ấn tượng ngay lần đầu. Bạn không cần import dữ liệu vào một bảng trước khi query. DuckDB đọc thẳng file như thể nó là một bảng:

import duckdb

# Query thẳng một file Parquet, không cần CREATE TABLE, không cần nạp
duckdb.sql("SELECT * FROM 'data/sao_ke_2026_01.parquet' LIMIT 5")

Đường dẫn có thể là một pattern glob để đọc nhiều file cùng lúc — cực kỳ tiện với dữ liệu được phân mảnh (partition) theo ngày/tháng:

duckdb.sql("""
    SELECT ma_chi_nhanh, SUM(so_tien) AS tong
    FROM 'data/sao_ke/*.parquet'
    GROUP BY ma_chi_nhanh
    ORDER BY tong DESC
""")

DuckDB tự động phát hiện định dạng và schema. Nó đọc được nhiều nguồn mà không cần cấu hình:

  • Parquet — định dạng cột nén, đọc chỉ đúng cột cần nhờ column pruning và bỏ qua các nhóm dòng không khớp điều kiện lọc (predicate pushdown).
  • CSV — với bộ dò kiểu (type sniffing) tự động đoán schema, xử lý được cả file méo mó.
  • JSON / JSON dòng (newline-delimited).
  • Apache Arrow / Polars / pandas — đọc thẳng đối tượng trong bộ nhớ Python (chi tiết bên dưới).

Vì DuckDB hiểu Parquet ở mức sâu, khi bạn viết WHERE thang = '2026-01' trên dữ liệu partition theo tháng, nó chỉ mở đúng những file/nhóm dòng liên quan chứ không quét toàn bộ. Đây là predicate pushdown — đẩy điều kiện lọc xuống tận tầng đọc file.

Đọc từ S3 và HTTP

DuckDB đọc trực tiếp từ object storage (S3, GCS, Azure Blob) và HTTP nhờ extension httpfs. Bạn có thể query một file Parquet trên S3 mà không tải về trước:

duckdb.sql("INSTALL httpfs; LOAD httpfs;")
duckdb.sql("SELECT COUNT(*) FROM 's3://ncb-datalake/sao_ke/2026/*.parquet'")

DuckDB chỉ kéo về các byte thực sự cần (nhờ HTTP range request), nên query một cột trên file lớn không đồng nghĩa với tải cả file.

Larger-than-memory

DuckDB không bị giới hạn bởi RAM. Khi một phép join hay sort vượt quá bộ nhớ, DuckDB tự động tràn (spill) ra đĩa và tiếp tục chạy thay vì báo lỗi hết bộ nhớ. Điều này cho phép xử lý dữ liệu lớn hơn RAM trên chính laptop của bạn — một điểm mạnh chung với chế độ streaming của Polars.

Dùng DuckDB trong Python

Có hai cách gọi phổ biến. Cách đơn giản nhất là duckdb.sql(...) dùng kết nối mặc định trong bộ nhớ:

import duckdb

res = duckdb.sql("SELECT 42 AS answer")
res.show()

Kết quả trả về là một đối tượng quan hệ (relation) lười — chỉ thực thi khi bạn yêu cầu vật chất hóa. Bạn chọn định dạng trả về tùy ý:

res = duckdb.sql("SELECT * FROM 'data/sao_ke/*.parquet'")

df_polars = res.pl()      # trả về Polars DataFrame
df_pandas = res.df()      # trả về pandas DataFrame
tbl_arrow = res.arrow()   # trả về Apache Arrow Table
rows      = res.fetchall()  # trả về list tuple Python

Truy vấn thẳng DataFrame như một bảng

Đây là chỗ tích hợp Polars/pandas với DuckDB tỏa sáng. Một DataFrame đang nằm trong biến Python có thể được tham chiếu thẳng bằng tên trong câu SQL — không cần đăng ký hay copy:

import duckdb
import polars as pl

# Một Polars DataFrame trong bộ nhớ
chi_nhanh = pl.DataFrame({
    "ma_chi_nhanh": ["CN01", "CN02", "CN03"],
    "ten": ["Hà Nội", "Hồ Chí Minh", "Đà Nẵng"],
})

# DuckDB "nhìn thấy" biến chi_nhanh như một bảng cùng tên
duckdb.sql("SELECT * FROM chi_nhanh WHERE ma_chi_nhanh = 'CN02'").pl()

DuckDB quét không gian biến của Python, tìm thấy chi_nhanh là một Polars DataFrame và đọc nó zero-copy qua Arrow — không tạo bản sao dữ liệu trong bộ nhớ. Cơ chế này áp dụng cho cả pandas DataFrame và Arrow Table.

Kết hợp Polars và DuckDB: dùng đúng công cụ cho từng phần

Trong thực tế nhiều pipeline dùng cả hai: SQL cho phần hợp nhất và tổng hợp (join nhiều nguồn, group by, window), Polars cho phần biến đổi cột phức tạp và feature engineering. Ví dụ hoàn chỉnh — query Parquet, join với một Polars DataFrame tham chiếu, rồi trả kết quả về Polars để xử lý tiếp:

import duckdb
import polars as pl

# Bảng tham chiếu nhỏ trong bộ nhớ (Polars)
chi_nhanh = pl.DataFrame({
    "ma_chi_nhanh": ["CN01", "CN02", "CN03"],
    "ten_chi_nhanh": ["Hà Nội", "Hồ Chí Minh", "Đà Nẵng"],
})

# DuckDB: đọc thẳng thư mục Parquet + join với DataFrame Polars
ket_qua = duckdb.sql("""
    SELECT
        c.ten_chi_nhanh,
        COUNT(*)          AS so_giao_dich,
        SUM(s.so_tien)    AS tong_tien
    FROM 'data/sao_ke/*.parquet' AS s
    JOIN chi_nhanh AS c USING (ma_chi_nhanh)
    GROUP BY c.ten_chi_nhanh
    ORDER BY tong_tien DESC
""").pl()   # trả về Polars DataFrame

# Tiếp tục biến đổi bằng Polars
ket_qua = ket_qua.with_columns(
    (pl.col("tong_tien") / pl.col("so_giao_dich")).alias("trung_binh")
)
print(ket_qua)

Lưu ý: các block SQL ở trên dùng cú pháp DuckDB (đọc file bằng đường dẫn, tham chiếu biến Python) — chúng không chạy được trên sandbox PostgreSQL của trang này, nên không được đánh dấu "▶ Chạy được". Đây là code minh họa để bạn chép về máy chạy thử.

Luồng dữ liệu tổng thể có thể hình dung như sau:

SQL "thân thiện" của DuckDB

Ngoài SQL chuẩn, DuckDB bổ sung nhiều cú pháp tiện lợi giúp câu truy vấn phân tích ngắn và ít lỗi hơn:

  • GROUP BY ALL — tự động group theo mọi cột không nằm trong hàm tổng hợp, khỏi phải liệt kê lại.
  • SELECT * EXCLUDE (cot_a, cot_b)* REPLACE (...) — chọn tất cả trừ vài cột, hoặc thay biểu thức cho một cột cụ thể mà không phải viết ra hàng chục tên cột.
  • QUALIFY — lọc theo kết quả window function mà không cần bọc subquery.
  • Kiểu phức hợp: LIST, STRUCT, MAP cho phép làm việc với dữ liệu lồng nhau (nested) đọc từ Parquet/JSON, rất hợp với dữ liệu bán cấu trúc.

Những chi tiết này nhỏ nhưng cộng lại giúp DuckDB dễ chịu hơn nhiều so với SQL truyền thống khi làm phân tích ad-hoc hằng ngày.

DuckDB vs Polars vs Warehouse: khi nào dùng gì

Ba công cụ này thường bị đặt lên bàn cân. Chúng bổ trợ nhau nhiều hơn là loại trừ.

Tiêu chíDuckDBPolarsWarehouse (BigQuery / Spark)
Giao diệnSQL đầy đủDataFrame / expressionSQL (phân tán) / API
Mô hình chạyNhúng, 1 máyNhúng, 1 máyCụm phân tán, có server
Quy môĐến hàng trăm GB / laptopĐến hàng trăm GB / laptopTB–PB
Điểm mạnhSQL trên file, join, ad-hocBiến đổi cột, pipelineDữ liệu khổng lồ, đồng thời cao
Hạ tầngKhông cầnKhông cầnCần vận hành cụm

Nguyên tắc chọn:

  • DuckDB — tuyệt vời cho phân tích cục bộ, prototyping, embedded analytics (nhúng phân tích thẳng trong ứng dụng), và cho những ai tư duy bằng SQL. Rất hợp để đọc lakehouse (đọc được cả bảng Iceberg và Delta Lake) ngay trên máy phân tích mà không cần dựng Spark.
  • Polars — khi bạn cần một pipeline biến đổi cột nhiều bước, feature engineering, hoặc muốn API DataFrame lập trình được. Xem lại bài Polars.
  • Warehouse / engine phân tán — khi dữ liệu vượt khả năng một máy, cần độ đồng thời cao cho nhiều người dùng, hoặc là kho dữ liệu trung tâm của tổ chức. Ví dụ BigQuery cho kho không-máy-chủ đám mây, hay Spark cho xử lý phân tán quy mô lớn.

Một mẫu thực dụng phổ biến ở ngân hàng: dùng warehouse/lakehouse làm nơi lưu trữ trung tâm, nhưng analyst kéo một lớp Parquet về (hoặc trỏ thẳng vào S3) rồi dùng DuckDB trên laptop để khám phá nhanh, không phải chờ hàng đợi cụm và không tốn chi phí quét của warehouse.

Các tính năng đáng chú ý

DuckDB không chỉ là "SELECT trên file". Nó là một SQL engine trưởng thành:

  • Window function đầy đủ: ROW_NUMBER, RANK, LAG/LEAD, khung OVER (PARTITION BY ... ORDER BY ...) — thiết yếu cho phân tích chuỗi thời gian và xếp hạng.
  • Hệ extension: cài thêm năng lực qua INSTALL/LOAD. Đáng chú ý: httpfs (S3/HTTP), iceberg (đọc bảng Apache Iceberg), delta (đọc Delta Lake), spatial, json, postgres/mysql/sqlite (query thẳng CSDL ngoài).
  • Đọc lakehouse: với extension tương ứng, DuckDB đọc bảng Iceberg và Delta trực tiếp — kể cả time travel — mà không cần engine phân tán.
  • COPY để ghi ra file: xuất kết quả truy vấn ra Parquet/CSV rất gọn:
duckdb.sql("""
    COPY (SELECT * FROM 'data/sao_ke/*.parquet' WHERE so_tien > 1000000)
    TO 'output/giao_dich_lon.parquet' (FORMAT PARQUET)
""")
  • Persist ra file .duckdb: ngoài chế độ in-memory, bạn có thể mở một CSDL trên đĩa để lưu bảng, view, index bền vững:
con = duckdb.connect("phan_tich.duckdb")
con.sql("CREATE TABLE tom_tat AS SELECT ma_chi_nhanh, SUM(so_tien) t FROM 'data/sao_ke/*.parquet' GROUP BY 1")
# Lần sau mở lại file này, bảng tom_tat vẫn còn

Một file .duckdb là một cột-store hoàn chỉnh: bạn có thể tạo nhiều bảng, view, ràng buộc, và chia sẻ nguyên file cho đồng nghiệp như chia sẻ một file SQLite.

Use case thực tế

Bối cảnh (số liệu ước lượng, minh họa): Đội phân tích rủi ro của NCB nhận dữ liệu sao kê giao dịch được data engineering xuất ra data lake dưới dạng Parquet, phân mảnh theo ngày: s3://ncb-datalake/sao_ke/dt=2026-01-01/*.parquet, ... Mỗi tháng khoảng 80–100 triệu dòng, tổng cỡ vài chục GB Parquet nén. Một analyst cần trả lời câu hỏi ad-hoc: "Trong tháng 1, top 20 chi nhánh theo tổng giá trị giao dịch chuyển khoản trên 500 triệu VND là những chi nhánh nào, kèm tên chi nhánh?"

Cách cũ (chậm): gửi yêu cầu cho team warehouse, chờ hàng đợi, hoặc bê toàn bộ file về pandas và tràn RAM sau vài phút.

Cách với DuckDB: analyst mở notebook ngay trên laptop 16 GB RAM. Bảng danh mục chi nhánh (khoảng 300 dòng) họ đã có sẵn trong một Polars DataFrame dm_chi_nhanh.

import duckdb, polars as pl

duckdb.sql("INSTALL httpfs; LOAD httpfs;")

dm_chi_nhanh = pl.read_csv("danh_muc/chi_nhanh.csv")  # ~300 dòng

top20 = duckdb.sql("""
    SELECT
        d.ten_chi_nhanh,
        COUNT(*)                       AS so_gd,
        SUM(s.so_tien)                 AS tong_gia_tri
    FROM 's3://ncb-datalake/sao_ke/dt=2026-01-*/*.parquet' AS s
    JOIN dm_chi_nhanh AS d USING (ma_chi_nhanh)
    WHERE s.loai_gd = 'CHUYEN_KHOAN'
      AND s.so_tien > 500000000
    GROUP BY d.ten_chi_nhanh
    ORDER BY tong_gia_tri DESC
    LIMIT 20
""").pl()

print(top20)

Vì sao nhanh: DuckDB dùng predicate pushdown — điều kiện dt=2026-01-* chỉ mở các file của tháng 1, và bộ lọc so_tien > 500000000 được đẩy xuống tầng đọc Parquet để bỏ qua các nhóm dòng không khớp; chỉ các cột ma_chi_nhanh, so_tien, loai_gd được đọc nhờ column pruning. Bảng dm_chi_nhanh được join zero-copy thẳng từ Polars. Kết quả trở về dưới dạng Polars DataFrame để analyst vẽ biểu đồ hay xuất Excel tiếp.

Kết quả ước lượng: truy vấn hoàn tất trong khoảng vài giây đến chục giây trên laptop, không tốn chi phí quét warehouse, không phải chờ hàng đợi cụm, và không tràn RAM nhờ cơ chế spill. Analyst lặp lại thử nghiệm với nhiều ngưỡng khác nhau chỉ bằng cách sửa câu SQL — vòng lặp khám phá dữ liệu ngắn hơn hẳn.

Ghi nhớ

  • DuckDB = "SQLite cho phân tích": CSDL OLAP nhúng, chạy trong tiến trình, không cần server. Cài bằng pip install duckdb.
  • Kiến trúc: lưu cột, thực thi vectorized, chạy đa lõi — cùng họ với Polars, nhưng giao diện là SQL đầy đủ.
  • Sức mạnh lớn nhất: chạy SQL thẳng trên Parquet/CSV/JSON bằng đường dẫn (kể cả glob nhiều file/partition) — không cần ETL/import trước.
  • Tích hợp Python liền mạch: duckdb.sql(...) tham chiếu thẳng Polars/pandas/Arrow DataFrame như bảng (zero-copy), trả kết quả về .pl() / .df() / .arrow().
  • Mẫu thực dụng: SQL (DuckDB) cho join và tổng hợp, Polars cho biến đổi cột và feature engineering — dùng cạnh nhau.
  • Đọc được S3/HTTP (httpfs), lakehouse Iceberg/Delta, hỗ trợ larger-than-memory (spill ra đĩa), window function, COPY, và persist ra file .duckdb.
  • Chọn công cụ: DuckDB cho phân tích cục bộ / prototyping / embedded và đọc lakehouse trên máy; warehouse phân tán (BigQuery/Spark) khi dữ liệu vượt một máy hoặc cần đồng thời cao.
  • Các block SQL của DuckDB dùng cú pháp riêng (đọc file, tham chiếu biến) — không chạy trên sandbox PostgreSQL của trang này.

Nguồn tham khảo

Bài viết liên quan

Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.

13 thg 7, 2026 6

Học cách tổ chức code Python: định nghĩa hàm với tham số vị trí/từ khoá/mặc định, *args/**kwargs, lambda và hàm bậc cao, closure, decorator, generator với yield. Đóng gói code thành module và package, cô lập thư viện bằng môi trường ảo venv, quản lý phụ thuộc với pip và requirements.txt để dự án tái lập được trên mọi máy.

13 thg 7, 2026 5

Biến script thành pipeline đáng tin cậy: cấu trúc project & packaging (uv/poetry), type hints & pydantic, kiểm thử với pytest, logging & cấu hình, đóng gói Docker, và tích hợp CI cho code dữ liệu.

13 thg 7, 2026 5

Hướng dẫn OOP trong Python từ class/instance, kế thừa và super(), đa hình & duck typing, encapsulation tới dunder methods, @property, classmethod/staticmethod, dataclass và type hints (mypy). Kèm nguyên tắc clean code: đặt tên rõ nghĩa, hàm nhỏ, DRY, SOLID cùng chuẩn PEP8 với công cụ ruff/black.

13 thg 7, 2026 5

Cảm nhận của bạn

Bình luận

Bạn cần để viết bình luận.

Chưa có bình luận. Hãy là người đầu tiên chia sẻ!