Vì sao DuckDB 2.0 nhanh hơn: Phân tích chi tiết từ bản alpha
DuckDB 2.0 alpha vừa ra mắt với tốc độ đọc dữ liệu từ S3 nhanh gấp 2-3 lần nhờ cơ chế async I/O, recursive CTE được viết lại giúp truy vấn đồ thị cha-con nhanh hơn hàng chục lần, cùng kiểu dữ liệu VARIANT mới giúp lưu trữ JSON tiết kiệm 2,7 lần dung lượng. Bài viết phân tích các tính năng quan trọng nhất kèm số liệu đo đạc thực tế.

DuckDB 2.0 dự kiến ra mắt vào mùa thu năm nay và bản alpha đã có sẵn để thử nghiệm. Đây là bước nâng cấp đáng chú ý đối với cơ sở dữ liệu phân tích đang được nhiều kỹ sư dữ liệu Việt Nam sử dụng cho các pipeline và mô hình dữ liệu quy mô vừa.
Điểm mấu chốt cần hiểu là: tốc độ tăng lên không chỉ nhờ tối ưu thuần túy, mà còn phụ thuộc vào cách bạn mô hình hóa dữ liệu. Dưới đây là ba tính năng quan trọng nhất cùng những con số đo đạc thực tế.
Biểu ngữ DuckDB 1.5
Async I/O: Truy vấn trên AWS S3 nhanh hơn đáng kể
Đây là cải tiến được mong đợi nhất vì bạn không cần thay đổi bất kỳ dòng truy vấn nào. Hãy xem ví dụ đọc một file Parquet 2,2 GB trên S3 (dữ liệu bình chọn Stack Overflow, 228 triệu dòng, 2268 row group) và đếm số phiếu theo loại. Truy vấn chỉ đọc một trong bốn cột, khoảng 230 MB.
CREATE SECRET s3 (TYPE s3, PROVIDER credential_chain, REGION 'us-east-1');
SET enable_external_file_cache = false; -- để mỗi lần chạy thực sự truy cập S3
SELECT VoteTypeId, count(*) AS n
FROM read_parquet('s3://us-prd-motherduck-open-datasets/stackoverflow/parquet/2023-05/votes.parquet')
GROUP BY ALL ORDER BY 1;
Cùng một truy vấn, cùng một máy tính xách tay:
- DuckDB 1.5.5: 18,8 giây
- DuckDB 2.0 alpha: 7,7 giây
Lưu ý: đây là kết quả đo qua kết nối internet gia đình, nên cả hai phiên bản đều bị ảnh hưởng như nhau. Nếu chạy từ hạ tầng cloud, con số sẽ nhanh hơn nhiều.
Cơ chế đằng sau là gì? File bị chia thành 2268 row group, mỗi nhóm khoảng 122.000 dòng. Với mỗi row group, DuckDB phải tải byte về, giải mã Parquet, đếm phiếu theo loại, rồi gộp kết quả cuối cùng. Có hai loại công việc: chờ mạng và xử lý CPU.
Ở phiên bản 1.5.5, mỗi trong số 18 worker luân phiên làm cả hai việc: tải, chờ, giải mã, tải, chờ. Trong lúc chờ mạng thì CPU nhàn rỗi, trong lúc giải mã thì không có lượt tải nào đang diễn ra, và số lượt tải đồng thời không bao giờ vượt quá 18.
Ở phiên bản 2.0, một nhóm luồng riêng chỉ chuyên tải dữ liệu, giữ hàng chục row group đang trên đường về và lưu tạm vào bộ đệm. Các worker chỉ giải mã, và luôn có sẵn row group để xử lý. Mạng và CPU hoạt động song song cùng lúc.
Cơ chế này được điều khiển bởi một tham số duy nhất: read_ahead_depth — số row group mà nhóm tải có thể đọc trước so với worker. Mặc định là -1 (tự động, tính theo số luồng), nên async I/O bật sẵn từ đầu. Đặt về 0 sẽ quay lại hành vi của phiên bản 1.5.
Kết quả đo trên S3:
- Một file Parquet 2,2 GB, một cột: từ 18,8 giây xuống 7,7 giây
- 23 file Parquet lớn, tổng 13,6 GB, một cột: từ 11,8 giây xuống 3,9 giây
- Một file CSV 1,7 GB: từ 116 giây xuống 55 giây
- 30 file Parquet nhỏ khoảng 1 MB: từ 3,7 giây xuống 3,3 giây (không cải thiện đáng kể)
Với các file nhỏ, thời gian chủ yếu nằm ở việc phải đi-về nhiều lần cho mỗi file (đọc footer rồi mới đọc dữ liệu), và đọc trước không giải quyết được vấn đề này. Việc lưu trữ data lake dưới dạng hàng nghìn file Parquet 1 MB vốn đã là thực hành kém, và bản 2.0 cũng không cứu được điều đó.
Tóm lại: đọc dữ liệu từ S3 nhanh hơn 2-3 lần ở phiên bản 2.0 mà không cần thay đổi truy vấn, nhờ nhóm luồng tải riêng chạy trước worker. Tính năng bật mặc định với
read_ahead_depth = -1.
Recursive CTE: Bước nhảy vọt cho dữ liệu cha-con nhiều tầng
Nhóm phát triển DuckDB đã viết lại engine recursive CTE và tuyên bố nhanh hơn 40 lần với bài toán duyệt đồ thị. Hãy cùng xem ý nghĩa thực tế qua một bảng dữ liệu quen thuộc.
Recursive CTE thực chất là một vòng lặp trên bảng. Lấy ví dụ bảng employees với hai cột: quản lý và nhân viên. Bạn muốn trả lời câu hỏi đơn giản: ai báo cáo cho ai.
| manager | employee |
|---|---|
| Ana | Ben |
| Ana | Cléa |
| Ben | Dev |
| Ben | Eli |
| Cléa | Fay |
| Dev | Gus |
| Fay | Hal |
Để duyệt cây, bạn bắt đầu với một dòng: Ana, tổng giám đốc. Vòng một, tìm mọi người có quản lý là Ana (Ben, Cléa). Vòng hai, tìm mọi người có quản lý là một trong những người đó (Dev, Eli, Fay). Tiếp tục cho đến khi không tìm thêm được ai mới. Mỗi tầng của sơ đồ tổ chức tương ứng với một vòng lặp.
WITH RECURSIVE team(person) AS (
SELECT 'Ana'
UNION
SELECT e.employee FROM team t JOIN employees e ON e.manager = t.person
)
SELECT count(*) FROM team;
Sơ đồ tổ chức, cây thư mục, bảng kê vật tư, chuỗi trả lời bình luận, phả hệ dữ liệu, lịch sử git — tất cả đều có dạng bảng hai cột cha-con. Khác biệt duy nhất là độ sâu, và độ sâu chính là số vòng lặp. Sơ đồ tổ chức có khoảng tám tầng, nhưng lịch sử git có thể lên tới hàng chục nghìn.
Vấn đề của phiên bản 1.5: mỗi vòng lặp, nó quay lại đọc toàn bộ bảng để tìm tầng tiếp theo. Tám tầng nghĩa là tám lần đọc toàn bảng. Hàng nghìn tầng nghĩa là hàng nghìn lần đọc cùng một bảng.
Giải pháp ở phiên bản 2.0: bảng chỉ được đọc một lần, một bảng tra cứu trên cột quản lý được xây dựng một lần, và mỗi vòng lặp chỉ tra cứu vài dòng vừa tìm được. Chi phí giờ đây tỉ lệ với số dòng thực sự chạm tới, chứ không phải số vòng nhân với kích thước bảng.
Đây là nơi lịch sử git thể hiện rõ lợi ích. Mỗi commit trỏ tới commit cha, nên bảng chỉ có hai cột commit_id và parent_id, và việc truy ngược tổ tiên của HEAD tốn một vòng cho mỗi commit. Với repo 20.000 commit:
WITH RECURSIVE ancestors(id) AS (
SELECT max(id) FROM commits -- HEAD
UNION
SELECT p.parent_id FROM ancestors a JOIN commit_parents p ON p.commit_id = a.id
)
SELECT count(*) FROM ancestors;
- DuckDB 1.5.5: từ 1,8 giây đến 16 giây tùy lần chạy
- DuckDB 2.0 alpha: 0,10 giây, ổn định mọi lần chạy
Tóm lại: nếu bạn duyệt chuỗi cha-con sâu (lịch sử git, phả hệ dữ liệu, chuỗi bình luận, bảng kê vật tư đầy đủ), phiên bản 2.0 biến một tác vụ tưởng chừng phải đẩy sang graph database thành một truy vấn bình thường.
VARIANT: Nhỏ hơn và nhanh hơn JSON
VARIANT giờ đây là kiểu dữ liệu hạng nhất trong DuckDB, và từ khóa cần nhớ là shredding.
Trong ngữ cảnh VARIANT, shredding nghĩa là: khi DuckDB ghi một row group xuống đĩa, nó xem xét cột JSON của bạn và tìm những trường xuất hiện trong hầu hết các dòng với cùng một kiểu giá trị. Ví dụ với một sự kiện trong số năm triệu sự kiện:
{"type": "purchase", "user": {"id": 42, "country": "FR", "premium": true},
"props": {"amount": 12.5, "currency": "EUR", "items": 2}, "tags": ["a", "b"]}
Loại sự kiện luôn là văn bản, user.id luôn là số, props.amount luôn là số thập phân. Những trường này được tách ra thành các cột thực sự bên dưới. Những trường hiếm gặp, hoặc trường là số ở dòng này nhưng là văn bản ở dòng khác, sẽ nằm chung trong phần dư nhị phân. Nhờ vậy, phần JSON nhất quán được lưu như bảng bình thường, chỉ phần lộn xộn mới lưu dạng blob.
So sánh lưu trữ năm triệu sự kiện theo ba cách:
| Cách lưu | Dung lượng trên đĩa |
|---|---|
| Chuỗi JSON | 224 MB |
| VARIANT (2.0 alpha) | 85 MB |
| Một cột định kiểu cho mỗi trường | 45 MB |
Và so sánh tốc độ truy vấn:
| Truy vấn | Chuỗi JSON | VARIANT 2.0 | Một cột định kiểu |
|---|---|---|---|
| Lọc trên hai trường | 366 ms | 63 ms | 52 ms |
| Tổng một trường số, nhóm theo quốc gia | 408 ms | 61 ms | 51 ms |
| Tra cứu danh sách | 357 ms | 1,96 s | 67 ms |
Những điểm đáng chú ý:
- VARIANT nhỏ hơn 2,7 lần so với chuỗi JSON.
- Với các truy vấn chạm vào trường đã được shred, VARIANT nhanh hơn khoảng 6 lần so với phân tích cú pháp JSON, và chỉ chậm hơn khoảng 20% so với cột định kiểu.
- So với VARIANT ở phiên bản 1.5.5, tốc độ nhanh hơn 78 lần, vì bản 1.5.5 có kiểu dữ liệu nhưng chưa có shredding.
- Truy vấn danh sách là ngoại lệ: ép kiểu danh sách VARIANT sang
VARCHAR[]tốn hai giây trong bản alpha này, còn chậm hơn cả đường JSON.
Trường hợp lý tưởng là log có cấu trúc. Các trường level, service, latency_ms, trace_id xuất hiện trong mọi dòng và luôn cùng kiểu giá trị, nên tất cả đều được shred. Cái bẫy cần tránh: latency_ms là 231 ở dòng này nhưng là "231ms" ở dòng sau sẽ rơi vào phần dư. Hãy giữ kiểu giá trị nhất quán.
Nguyên tắc vàng khi mô hình hóa vẫn giữ nguyên: hãy mô hình hóa những gì bạn biết. Những trường mà mọi truy vấn đều chạm tới xứng đáng có cột thật, và việc nâng cấp chỉ tốn hai câu lệnh:
ALTER TABLE ev ADD COLUMN type VARCHAR;
UPDATE ev SET type = payload.type::VARCHAR;
Tóm lại: nếu các sự kiện của bạn chia sẻ một tập trường nhất quán với kiểu giá trị ổn định, hãy lưu chúng dưới dạng VARIANT thay vì chuỗi JSON. Bạn tiết kiệm một phần ba dung lượng và có các truy vấn trường chạy như cột thật. Hãy nâng các trường được truy vấn thường xuyên thành cột thật, giữ phần đuôi dài trong VARIANT, và tạm thời tránh ép kiểu danh sách trong các truy vấn nóng.
Biểu đồ tối ưu hiệu năng truy vấn
Những viên ngọc ẩn khác trong bản phát hành
Trigger. Bảng có thể tự chạy một đoạn SQL khi các dòng thay đổi. Ví dụ, khi cập nhật giá, trigger sẽ thấy từng dòng trước và sau khi thay đổi (gọi là "bảng chuyển tiếp") và ghi cả hai vào bảng lịch sử.
CREATE TRIGGER price_history AFTER UPDATE ON prices
REFERENCING OLD TABLE AS before_rows NEW TABLE AS after_rows FOR EACH STATEMENT
INSERT INTO price_history (id, old_price, new_price)
SELECT a.id, b.price, a.price FROM before_rows b JOIN after_rows a USING (id);
Schema lồng nhau: CREATE SCHEMA finance.reports; và các bảng bên trong đó.
DML trong CTE: một lệnh DELETE ... RETURNING bên trong WITH, rồi INSERT từ đó — đây chính là mẫu di chuyển dòng một cách nguyên tử.
WITH moved AS MATERIALIZED (DELETE FROM staging RETURNING *)
INSERT INTO prod SELECT * FROM moved;
Các tính năng thú vị khác:
SET dialect_compatibility_mode = 'spark': chế độ tương thích Spark SQL cho những ai cần chuyển một số pipeline từ Spark SQL sang DuckDB.SET external_file_cache_spill = true: DuckDB lưu đệm các khối file từ xa trong bộ nhớ; khi bị đẩy ra, tính năng này ghi chúng xuống thư mục tạm thay vì tải lại. Với giới hạn bộ nhớ 300 MB, lần đọc thứ hai của một file Parquet 854 MB trên S3 giảm từ 23,9 giây xuống 0,35 giây.- CLI có thêm trình định dạng SQL (
duckdb -formathoặc.auto_format on), lịch sử truy vấn có thể truy vấn được (.history,FROM shell_history()), cùng.aboutvà.manual. read_jsongiờ nhận diện dấu thời gian ISO-8601 có offset làTIMESTAMPTZthay vì âm thầm bỏ qua offset.CREATE SECRET s IN CONNECTION (...)giới hạn một secret cho một kết nối.- Quack — giao thức client-server — là nửa còn lại của bản phát hành này và đạt phiên bản 1.0 cùng lúc.
Thử nghiệm và phản hồi
Cài đặt bản alpha chỉ với một dòng lệnh cho CLI, các client khác có trên trang cài đặt của DuckDB:
curl https://install.duckdb.org | DUCKDB_VERSION=alpha bash
~/.duckdb/cli/latest/duckdb -c "SELECT version()"
MotherDuck sẽ hỗ trợ phiên bản 2.0 gần thời điểm phát hành chính thức, nên bạn có thể trải nghiệm ngay trên nền tảng cloud và tận hưởng tất cả những cải tiến đã đề cập.
Đối với các kỹ sư dữ liệu tại Việt Nam đang cân nhắc xây dựng data lakehouse quy mô vừa, DuckDB 2.0 mang đến một lựa chọn đáng cân nhắc: chi phí hạ tầng thấp, tốc độ truy vấn trên S3 cải thiện rõ rệt, và khả năng xử lý dữ liệu JSON bán cấu trúc — vốn rất phổ biến trong các hệ thống log và event tracking — trở nên thực tế hơn nhiều.


