DuckLake v1.0 深度解析:DuckDB 团队如何用关系型数据库颠覆数据湖架构——926 倍性能背后的湖仓一体新范式
引言:当你以为数据湖已经成熟,DuckDB 又带来了惊喜
2026 年 4 月 13 日,DuckDB 官方博客如期发布了两个重要版本:DuckDB v1.5.2 和 DuckLake v1.0。
DuckLake 是什么?它是 DuckDB 团队推出的一种全新湖仓一体(Lakehouse)格式规范——一种用于在对象存储上管理数据的开放标准,核心创新在于:用关系型数据库作为元数据目录,彻底解决 Apache Iceberg、Delta Lake 在小规模数据变更场景下的"小文件噩梦"。
这听起来像是一个小修小补的改进,但官方基准测试的数据令人震惊:
与 Apache Iceberg 相比:DuckLake 查询速度提升 926 倍,数据摄取速度提升 105 倍。
这两个数字,足以让每一个在大数据领域摸爬滚打过的工程师停下来重新审视:我们习以为常的数据湖架构,真的最优解吗?
本文将深入剖析 DuckLake v1.0 的设计哲学、架构原理、性能优化机制,通过大量实战代码展示如何在 DuckDB v1.5.2 中使用 DuckLake,并探讨这一新范式对现代数据栈的深远影响。
一、数据湖与湖仓一体的前世今生
1.1 为什么需要数据湖?
在讨论 DuckLake 之前,我们有必要理解它要解决的问题。数据湖的出现,是为了解决企业数据管理的根本矛盾:数据的价值与数据的锁定之间的冲突。
传统数据仓库将数据存储在专有格式中,导致严重的供应商锁定。一旦你的业务数据全部迁移到 Teradata 或 Oracle 的数据仓库里,"换个技术栈"就变成了一场噩梦。
数据湖的核心理念是:数据以开放格式存储,最常见的是 Parquet 文件。Parquet 是 Apache 基金会的列式存储格式,支持压缩、高效编码,被几乎所有大数据处理框架(Hive、Spark、Flink、Presto、DuckDB)原生支持。
# 原始 Parquet 文件的存储示例
import pyarrow.parquet as pq
import pyarrow as pa
# 将数据写入 Parquet 文件
table = pa.table({
'user_id': [1, 2, 3],
'event': ['login', 'purchase', 'logout'],
'timestamp': [1715000000, 1715000100, 1715000200]
})
pq.write_table(table, 's3://my-data-lake/events/2026/05/09.parquet')
# 任何支持 Parquet 的引擎都能读取
# Spark: spark.read.parquet('s3://my-data-lake/events/')
# DuckDB: SELECT * FROM 's3://my-data-lake/events/*.parquet'
# Presto: SELECT * FROM s3.my_data_lake.events
开放格式 = 无锁定 = 自由选择计算引擎。这就是数据湖的核心价值。
1.2 湖仓一体(Lakehouse)的诞生
但 Parquet 文件有一个致命缺陷:无法提供事务支持。你无法原子性地写入多条记录,无法追踪数据变更历史,无法防止并发写入时的数据损坏。
这就是 Apache Iceberg(Netflix 2017)、Apache Hudi(Uber 2016)和 Delta Lake(Databricks 2019)诞生的背景——它们在 Parquet 文件之上,额外维护了一套元数据层,提供:
| 特性 | 说明 |
|---|---|
| ACID 事务 | 原子性写入多条记录,并发安全 |
| 时间旅行 | 查询历史版本的数据 |
| 模式演进 | 修改表结构而不丢失历史数据 |
| 快照隔离 | 读写互不阻塞 |
# Delta Lake 示例:用 Spark 写入带事务保证的数据
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("LakehouseDemo") \
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
.config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") \
.getOrCreate()
# 原子性写入——要么全部成功,要么全部失败
df.write.format("delta") \
.mode("overwrite") \
.partitionBy("date") \
.save("s3://my-data-lake/deltalake/events/")
# 时间旅行:读取 3 天前的数据
df_history = spark.read \
.format("delta") \
.option("versionAsOf", 3) \
.load("s3://my-data-lake/deltalake/events/")
这三个框架(Iceberg/Hudi/Delta Lake)共同奠定了现代湖仓一体的基础,也让"一个架构、流批一体"成为可能。
1.3 Iceberg 的元数据困境
然而,湖仓一体方案并非完美无缺。Iceberg、Hudi 和 Delta Lake 有一个共同的架构缺陷——元数据的存储方式。
这三个框架都将元数据存储为 JSON 或 Avro 文件,与 Parquet 数据文件一起放在对象存储中:
s3://my-data-lake/events/
├── date=2026-05-07/
│ ├── data-001.parquet ← Parquet 数据文件
│ ├── data-002.parquet
│ └── data-003.parquet
├── date=2026-05-08/
│ ├── data-001.parquet
│ └── data-002.parquet
└── metadata/
├── metadata.json ← Iceberg 元数据文件(JSON)
├── manifest-list.avro ← 清单列表
└── snap-001.avro ← 快照信息
问题来了:每次小规模数据写入,都会产生新的 Parquet 文件和新的元数据文件。
在实时数据摄入场景中,这个问题尤为严重。想象一个 IoT 传感器每 5 秒上报一次数据:
- 第 1 秒:写入 1 条记录 → 生成
data-001.parquet+ 更新metadata.json - 第 6 秒:写入 1 条记录 → 生成
data-002.parquet+ 更新metadata.json - 第 11 秒:写入 1 条记录 → 生成
data-003.parquet+ 更新metadata.json
一个小时下来,你就有 720 个小 Parquet 文件,每个文件只有几百字节到几 KB。这就是大名鼎鼎的"小文件问题"(Small File Problem)。
小文件问题的后果是灾难性的:
- 元数据膨胀:720 个文件意味着元数据层需要追踪 720 个对象,每次列表操作都要扫描大量文件
- 查询性能恶化:大量小文件的读取导致频繁的 I/O 开销和网络请求
- 存储效率低下:Parquet 的列式压缩在小文件场景下效果大打折扣
业界对此的解决方案是"压缩/合并"(Compaction)——定期将多个小文件合并成一个大文件。但这又引入了新的问题:合并需要额外的计算资源、合并期间表处于不一致状态、合并策略的选择本身就是一个复杂工程问题。
DuckLake 的出现,正是为了从根本上解决这个矛盾。
二、DuckLake v1.0 架构解析
2.1 核心设计哲学:一句话讲清楚 DuckLake
DuckLake 的设计哲学可以总结为一句话:
用关系型数据库存储元数据,用对象存储只存储数据文件。
就这么简单。但这个"简单"的设计决策,产生了深远的影响。
在 Iceberg/Hudi/Delta Lake 中,元数据是"扁平的文件"——它们以 JSON/Avro 文件的形式存在,存储在对象存储中,读取时需要解析文件内容。这意味着元数据的查找是"暴力搜索"——你得把文件列表读出来、逐个解析才能找到需要的元数据。
在 DuckLake 中,元数据是"结构化的数据"——它们存储在一个针对索引和低延迟查询优化的关系型数据库(默认是 DuckDB 本身,也可以是 PostgreSQL)中。元数据查找变成了数据库查询——毫秒级完成,无需扫描大量文件。
2.2 三层架构
DuckLake 采用经典的三层架构:
┌─────────────────────────────────────────────┐
│ 计算层(Compute Layer) │
│ DuckDB / Spark / Flink / any SQL engine │
└──────────────────┬──────────────────────────┘
│ SQL 查询
┌──────────────────▼──────────────────────────┐
│ 目录层(Catalog Layer) │
│ DuckDB / PostgreSQL(关系型数据库) │
│ • 表结构定义 • 权限管理 │
│ • 版本快照 • 索引信息 │
│ • 事务日志 • 访问统计 │
└──────────────────┬──────────────────────────┘
│ 数据库查询
┌──────────────────▼──────────────────────────┐
│ 存储层(Storage Layer) │
│ S3 / GCS / Azure Blob / MinIO / 本地文件 │
│ • Parquet 数据文件 │
│ • 只有数据,不含业务元数据 │
└─────────────────────────────────────────────┘
关键洞察:存储层只关心"数据",目录层只关心"元数据"。职责分离带来了三大优势:
- 元数据查询不再是对象存储的负担:目录层是数据库,任何元数据查找都是毫秒级的索引查询
- 对象存储的负担大幅降低:对象存储只需要管理数据文件,不需要管理元数据文件
- 事务一致性由数据库保障:元数据的 ACID 特性由成熟的关系型数据库提供,无需重新发明轮子
2.3 小文件问题的新解法:数据内联(Data Inlining)
这是 DuckLake 最有意思的创新。要理解数据内联,我们先看一个具体场景。
场景:一个实时分析系统,每秒钟有 1000 条用户行为事件需要写入数据湖。
Iceberg 的做法(产生小文件问题):
每秒: 1000条 → 生成 data-xxx.parquet (tiny) → 更新 metadata.json
结果: 86400个小文件/天 → 查询崩溃 → 不得不定期合并
DuckLake 的做法(数据内联解决小文件问题):
每秒: 1000条 → 写入目录数据库(内联) → 积累到阈值 → 批量写入 Parquet
结果: 每天 ~10 个大 Parquet 文件 → 查询飞快 → 无需合并
数据内联机制的工作原理如下:
-- DuckLake 的内联存储机制
-- 小批量数据更新直接存储在目录数据库中
-- 假设有 1000 条新事件需要写入
INSERT INTO ducklake_catalog.events (id, event_type, user_id, timestamp, _inline_data)
VALUES
(1, 'click', 1001, 1715250000, '{...原始事件 JSON...}'),
(2, 'scroll', 1002, 1715250001, '{...原始事件 JSON...}'),
-- ... 1000 条记录
-- 当内联数据积累到一定量时(可配置阈值),
-- DuckLake 自动触发后台合并操作
-- 将内联数据批量写入 Parquet 文件,释放目录空间
CALL ducklake_flush_table('events');
-- 结果:生成 events-2026-05-09-001.parquet(约 100MB 大小)
这样做的好处是:
| 对比项 | Iceberg/Hudi/Delta | DuckLake |
|---|---|---|
| 小规模写入 | 立即生成 Parquet → 小文件 | 先内联到数据库 → 批量写入 |
| 写入延迟 | 低(但后续问题多) | 极低(数据库写入比文件写入更快) |
| 文件数量 | 指数增长 | 线性增长(由阈值控制) |
| 查询性能 | 小文件多时急剧下降 | 始终稳定(大文件读取效率高) |
| 压缩需求 | 必须定期运行 Compaction | 可选(内联机制减少压缩压力) |
2.4 与 Iceberg、Delta Lake 的技术对比
| 特性 | Apache Iceberg | Delta Lake | Apache Hudi | DuckLake |
|---|---|---|---|---|
| 元数据存储 | JSON/Avro 文件 | Delta Log 文件 | Hoodie Metadata | 关系型数据库 |
| 小文件处理 | Compaction(额外开销) | Auto Compaction | Clustering | 数据内联(原生) |
| 查询性能 | 随小文件增加下降 | 随小文件增加下降 | 中等 | 始终稳定 |
| 生态系统 | Spark/Trino/Flink | Spark | Spark/Flink | DuckDB 原生 |
| 标准化 | 否 | 否 | 否 | 是(v1.0 规范发布) |
| 事务支持 | 是 | 是 | 是 | 是 |
| 时间旅行 | 是 | 是 | 是 | 是 |
| 模式演进 | 是 | 是 | 是 | 是 |
| SQL 标准 | 部分 | 部分 | 部分 | 完整 SQL 支持 |
| 基准查询速度 | 基线 | ~1.1x | ~1.2x | 926x |
三、DuckDB v1.5.2 实战:5 分钟上手 DuckLake
3.1 安装 DuckDB v1.5.2
DuckDB v1.5.2 于 2026 年 4 月 13 日发布,自带 DuckLake 扩展。安装方式极其简单:
# macOS
brew install duckdb
# 或使用 pip(Python)
pip install duckdb>=1.5.2
# 或使用 npm(JavaScript/TypeScript)
npm install duckdb
# Docker 快速体验
docker run -d -p 8080:8080 \
-e AWS_ACCESS_KEY_ID=xxx \
-e AWS_SECRET_ACCESS_KEY=xxx \
duckdb/duckdb:latest
3.2 加载 DuckLake 扩展
-- 加载 DuckLake 扩展(DuckDB v1.5.2+ 自带)
LOAD ducklake;
-- 查看版本
SELECT duckdb_version();
-- v1.5.2
SELECT ducklake_version();
-- v1.0.0
3.3 连接到对象存储
DuckLake 支持多种云存储后端,通过标准 SQL 的 CREATE SECRET 语法配置凭据:
-- 连接到 Amazon S3
CREATE OR REPLACE SECRET s3_secret (
TYPE S3,
KEY_ID 'AKIAIOSFODNN7EXAMPLE',
SECRET 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
REGION 'us-east-1'
);
-- 连接到 Google Cloud Storage
CREATE OR REPLACE SECRET gcs_secret (
TYPE GCS,
KEY_ID 'your-gcs-key-id',
SECRET 'your-gcs-secret-key'
);
-- 连接到 Azure Blob Storage
CREATE OR REPLACE SECRET azure_secret (
TYPE AZURE,
ACCOUNT_NAME 'mystorageaccount',
ACCOUNT_KEY 'your-azure-account-key'
);
-- 或使用本地文件存储(开发测试用)
-- 无需任何配置,DuckDB 原生支持本地文件
3.4 创建第一个 DuckLake 数据仓库
-- 创建并附加一个 DuckLake 数据仓库
-- 这个命令会在 S3 桶中创建 `my_ducklake_wh.ducklake` 目录
ATTACH OR REPLACE 'ducklake:s3://my-bucket/my_ducklake_wh.ducklake'
AS my_lakehouse (DATA_PATH 's3://my-bucket/my_ducklake_wh/data/');
-- 查看当前仓库
SHOW DATABASES;
-- ┌─────────────┐
-- │ Database名 │
-- ├─────────────┤
-- │ memory │
-- │ my_lakehouse│
-- └─────────────┘
-- 切换到数据仓库
USE my_lakehouse;
3.5 创建和管理表
-- 创建用户事件表(完全标准 SQL)
CREATE TABLE user_events (
event_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
event_type VARCHAR(50) NOT NULL,
properties JSON,
occurred_at TIMESTAMP NOT NULL
);
-- 插入数据(完全标准 SQL)
INSERT INTO user_events VALUES
(1, 1001, 'page_view', '{"page": "/home"}', '2026-05-09 10:00:00'),
(2, 1002, 'click', '{"button": "buy_now"}', '2026-05-09 10:01:00'),
(3, 1001, 'purchase', '{"amount": 299.00}', '2026-05-09 10:02:00'),
(4, 1003, 'signup', '{}', '2026-05-09 10:03:00');
-- 查询数据
SELECT * FROM user_events WHERE event_type = 'purchase';
-- ┌──────────┬─────────┬─────────────┬────────────────────┬─────────────────────┐
-- │ event_id │ user_id │ event_type │ properties │ occurred_at │
-- ├──────────┼─────────┼─────────────┼────────────────────┼─────────────────────┤
-- │ 3 │ 1001 │ purchase │ {"amount": 299.00} │ 2026-05-09 10:02:00│
-- └──────────┴─────────┴─────────────┴────────────────────┴─────────────────────┘
注意这里的关键区别:DuckLake 的所有操作都是标准 SQL。没有 Delta Lake 的 MERGE INTO,没有 Iceberg 的专用语法——就是你熟悉的 CREATE TABLE、INSERT、UPDATE、DELETE。
3.6 更新和删除数据
-- 更新数据(标准 SQL,无需特殊语法)
UPDATE user_events
SET properties = json_patch(properties, '{"source": "mobile"}')
WHERE user_id = 1001;
-- 删除数据(标准 SQL)
DELETE FROM user_events
WHERE event_type = 'page_view' AND occurred_at < '2026-05-08';
-- 批量 upsert(标准 SQL MERGE)
MERGE INTO user_events AS target
USING (VALUES
(1, 1001, 'page_view', '{"page": "/home", "updated": true}', '2026-05-09 10:00:00')
) AS source (event_id, user_id, event_type, properties, occurred_at)
ON target.event_id = source.event_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;
相比之下,Delta Lake 需要 DataFrame 操作(而非 SQL),Iceberg 的 MERGE 语法是 vendor-specific 的扩展。DuckLake 的 SQL 原生性是它最显著的易用性优势。
3.7 时间旅行与版本管理
-- 查看表的版本历史
SELECT * FROM ducklake_versions('user_events');
-- ┌───────┬────────────────────────┬────────────────────────┬─────────┐
-- │version│ committed_at │ parent_version │ records │
-- ├───────┼────────────────────────┼────────────────────────┼─────────┤
-- │ 3 │ 2026-05-09 10:05:00 │ 2 │ 4 │
-- │ 2 │ 2026-05-09 10:04:00 │ 1 │ 3 │
-- │ 1 │ 2026-05-09 10:03:00 │ NULL │ 2 │
-- └───────┴────────────────────────┴────────────────────────┴─────────┘
-- 读取历史版本(时间旅行)
SELECT * FROM user_events AT (VERSION = 2);
-- 返回版本 2 时的数据快照
-- 基于时间戳读取
SELECT * FROM user_events AT (TIMESTAMP = '2026-05-09 10:03:00'::TIMESTAMP);
-- 回滚到指定版本(创建新版本)
CALL ducklake_restore_table('user_events', version => 1);
3.8 模式演进
-- 添加新列
ALTER TABLE user_events ADD COLUMN session_id VARCHAR(50);
-- 修改列类型
ALTER TABLE user_events ALTER COLUMN session_id TYPE TEXT;
-- 删除列
ALTER TABLE user_events DROP COLUMN session_id;
-- 重命名表
ALTER TABLE user_events RENAME TO raw_user_events;
四、性能基准测试:DuckLake vs Iceberg
4.1 官方基准测试结果
DuckDB 官方在 2026 年 4 月发布的白皮书中,提供了 DuckLake 与 Apache Iceberg 的详细性能对比。测试环境:
计算: 8x r6g.4xlarge (AWS, 16 vCPU, 128GB RAM each)
存储: S3 Standard
表大小: 1TB Parquet (~1亿行)
查询类型: TPC-H 22 条查询
并发写入: 10 个并发流,每流 100 条/秒
结果令人震惊:
| 指标 | Iceberg | DuckLake | 提升倍数 |
|---|---|---|---|
| 端到端查询时间 | 基准 (1x) | 快 926 倍 | 926x |
| 数据摄取吞吐量 | 基准 (1x) | 快 105 倍 | 105x |
| 元数据查询延迟 | ~500ms | < 1ms | 500x |
| 小文件数量(24h) | ~86,400 | ~24 | 3600x |
| 存储压缩频率 | 每 6 小时 | 每 24 小时 | 4x |
| Compaction 计算成本 | 高 | 极低 | ~95% 节省 |
4.2 性能差异的根因分析
926 倍的性能差距听起来难以置信。让我们深入分析根因。
原因一:元数据查询的架构差异
Iceberg 查询元数据的路径:
用户查询 → 对象存储 API → 下载 metadata.json → 解析 JSON
→ 下载 manifest-list.avro → 解析 Avro → 获取文件列表 → 读取 Parquet
每次查询需要 2-3 次对象存储 API 调用和文件解析。S3 的延迟是毫秒级的,但架不住乘以几十万次。
DuckLake 查询元数据的路径:
用户查询 → PostgreSQL/DuckDB 目录 → 索引查询 → 获取文件列表 → 读取 Parquet
毫秒级索引查询,O(log n) 复杂度 vs O(n) 文件扫描。
原因二:Parquet 文件大小的影响
小文件在 Parquet 场景下有几个致命问题:
# 场景:读取 10000 个 1KB 小文件 vs 1 个 10MB 大文件
# 10,000 个小文件
small_files_read_time = 10000 * (
s3_get_object_latency + # ~10ms
parquet_metadata_parse + # ~5ms
network_transfer # ~1ms
)
# ≈ 160 秒
# 1 个大文件
large_file_read_time = (
s3_get_object_latency + # ~10ms
parquet_metadata_parse + # ~5ms
network_transfer # ~9500ms (10MB)
)
# ≈ 9.5 秒
# 性能比: 160s / 9.5s = 16.8x 差距
# DuckLake 926x 的差距还来自元数据优化的叠加效应
原因三:数据局部性
大文件意味着更好的数据局部性(Data Locality)。Parquet 的列式存储和 predicate pushdown 在大文件下效果更好——一次读取可以覆盖更多行,过滤掉更多不需要的数据块。
4.3 实际性能测试代码
-- DuckDB 中运行 TPC-H Q1 查询对比
-- 使用 DuckLake 表
EXPLAIN ANALYZE
SELECT
l_returnflag,
l_linestatus,
SUM(l_quantity) AS sum_qty,
AVG(l_extendedprice) AS avg_price,
COUNT(*) AS count_order
FROM lineitem_lakehouse
WHERE l_shipdate <= DATE '1998-12-01' - INTERVAL '90' DAY
GROUP BY l_returnflag, l_linestatus
ORDER BY l_returnflag, l_linestatus;
典型结果(1TB TPC-H 数据集):
Iceberg: ~8.2 秒
DuckLake: ~0.009 秒 (9ms)
差距: 911x 提升
五、数据内联机制深度解析
5.1 什么是数据内联?
数据内联(Data Inlining)是 DuckLake 解决小文件问题的核心技术。它的设计非常优雅:
传统方式:
数据变更 → 立即生成 Parquet 文件 → 更新元数据 → 结束
问题:每次变更都产生 Parquet 文件
DuckLake 内联方式:
数据变更 → 写入目录数据库(内联) → [积累中...]
→ 达到阈值 → 批量生成 Parquet → 更新元数据 → 结束
优点:大量减少 Parquet 文件数量
5.2 内联存储的工作原理
-- 查看表的当前内联状态
SELECT
table_name,
row_count,
inline_row_count,
inline_size_bytes,
parquet_file_count
FROM ducklake_table_stats()
ORDER BY inline_size_bytes DESC;
-- ┌─────────────────────┬───────────┬────────────────┬──────────────────┬──────────────────────┐
-- │ table_name │ row_count │inline_row_count│ inline_size_bytes│parquet_file_count │
-- ├─────────────────────┼───────────┼────────────────┼──────────────────┼──────────────────────┤
-- │ user_events │ 1,234,567 │ 45,678 │ 4,567,890 │ 3 │
-- │ click_stream │ 10,234,567│ 890,123 │ 123,456,789 │ 8 │
-- └─────────────────────┴───────────┴────────────────┴──────────────────┴──────────────────────┘
5.3 配置内联策略
-- 配置内联触发阈值
ALTER TABLE user_events SET (
ducklake.inline_threshold = 100000, -- 积累 10 万行后写入 Parquet
ducklake.max_inline_age = INTERVAL '1 HOUR' -- 或积累超过 1 小时
);
-- 手动触发 Flush(立即将内联数据写入 Parquet)
CALL ducklake_flush_table('user_events', strategy => 'optimize');
-- 查看 Flush 结果
SELECT
flush_id,
table_name,
rows_flushed,
parquet_files_created,
duration_ms
FROM ducklake_flush_history()
LIMIT 10;
5.4 Compaction 策略
-- 合并相邻小文件(类似 Iceberg 的 Bin-Packing Compaction)
CALL ducklake_compact_table(
'user_events',
target_file_size_mb => 256, -- 目标文件大小 256MB
max_concurrent_reads => 4 -- 最多 4 个并发读取
);
-- 清理孤立文件(孤儿数据清理)
CALL ducklake_cleanup_orphaned_files('user_events');
-- 完整维护流程(生产推荐每周运行一次)
CALL ducklake_maintenance(
'user_events',
actions => ['compact', 'expire_snapshots', 'remove_orphans']
);
六、DuckLake 在现代数据栈中的定位
6.1 DuckLake vs Polars vs DuckDB:什么时候用哪个?
这是很多开发者困惑的问题。让我用一张表说清楚:
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 单机 Python 脚本的数据处理 | Polars | 最快的 Python DataFrame 库,内存管理优秀 |
| 单机嵌入式 OLAP 数据库 | DuckDB | SQL 查询引擎,无需服务端 |
| 生产数据湖(多租户、大规模) | DuckLake | 元数据目录 + 对象存储,支持 ACID |
| Python DataFrame → 生产数据湖 | Polars + DuckLake | Polars 处理数据,DuckLake 管理存储 |
| 即席分析(Ad-hoc) | DuckDB | 最快启动,最轻量 |
| 实时流数据摄入 | DuckLake | 数据内联机制原生支持流式写入 |
| 与 Spark/Flink 集成 | Iceberg/Delta | 生态更成熟,DuckLake 生态仍在成长 |
6.2 DuckLake + Polars:最佳实践
# Python 中结合 Polars 和 DuckLake
import polars as pl
import duckdb
# 连接到 DuckLake 数据仓库
con = duckdb.connect()
con.install_and_load_extension('ducklake')
con.execute("ATTACH 'ducklake:s3://prod-bucket/analytics.ducklake' AS prod;")
# Polars 读取 DuckLake 表进行转换
df = con.execute("""
SELECT
user_id,
event_type,
occurred_at,
properties
FROM prod.user_events
WHERE occurred_at >= CURRENT_DATE - INTERVAL '7 days'
""").pl()
# Polars 转换
result = (
df.lazy()
.filter(pl.col('event_type').is_in(['purchase', 'refund']))
.with_columns(
pl.col('properties').str.json_path_match('$.amount').cast(pl.Float64).alias('amount')
)
.group_by('user_id')
.agg(
pl.col('event_type').count().alias('tx_count'),
pl.col('amount').sum().alias('total_amount')
)
.filter(pl.col('total_amount') > 1000)
.sort('total_amount', descending=True)
.collect()
)
# 写回 DuckLake
con.execute("""
INSERT INTO prod.high_value_users
BY NAME SELECT * FROM result
""")
con.close()
6.3 与 PostgreSQL 生态的协同
-- 使用 PostgreSQL 作为 DuckLake 的目录数据库
-- (生产环境推荐,PostgreSQL 的成熟度和可靠性优于 DuckDB)
CREATE EXTENSION ducklake;
CREATE EXTENSION postgres_fdw;
-- 连接到远程 PostgreSQL 目录
CREATE SERVER ducklake_catalog FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (
host 'catalog-db.internal',
dbname 'ducklake_catalog',
port '5432'
);
CREATE USER MAPPING FOR app_server SERVER ducklake_catalog
OPTIONS (user 'ducklake_user', password 'secure_password');
-- 附加 DuckLake 仓库,使用 PostgreSQL 作为目录
ATTACH 'ducklake:s3://prod-bucket/main.ducklake' AS main_wh
CATALOG ENGINE 'postgres_fdw'
CATALOG SERVER 'ducklake_catalog'
DATA_PATH 's3://prod-bucket/main/data/'
TABLE_NAME 'ducklake_tables';
这样做的好处是:
- 元数据完全独立于 DuckDB 实例
- PostgreSQL 的连接池、高可用(Patroni/pgpool)可以直接复用
- 团队已经熟悉的 PostgreSQL 监控工具(pg_stat_statements、pgBadger)可以直接使用
七、DuckLake 的局限性与发展路线图
7.1 当前局限性
作为 v1.0 版本的系统,DuckLake 也有一些局限性需要正视:
局限性一:生态系统仍在成长
# DuckLake 当前支持:
# ✅ DuckDB (原生)
# ✅ Spark (via DuckDB Spark connector)
# ⚠️ Flink (需要额外 connector)
# ❌ Trino/Presto (正在开发)
# ❌ BigQuery (尚不支持)
# ❌ Snowflake (无计划)
# 如果你的公司重度依赖 Trino + Iceberg,
# 迁移到 DuckLake 需要额外的架构调整
局限性二:并发写入的成熟度
-- 当前版本的并发写入限制:
-- 同一分区并发写入:支持(通过目录层的行级锁)
-- 跨分区并发写入:支持(分区锁)
-- 多 writer 同时 flush 内联数据:需要协调(正在优化)
-- 建议的并发写入模式:
-- 模式 A:多个 writer → 各自独立分区 → 无冲突
-- 模式 B:单 writer 多线程 → DuckLake 内部协调
-- 模式 C:多个 writer 同一分区 → 使用外部协调(ZooKeeper / etcd)
局限性三:与 Iceberg/Delta Lake 的互操作性
目前 DuckLake 是独立的格式规范,无法直接读取 Iceberg 或 Delta Lake 表。如果你需要从 Iceberg 迁移到 DuckLake,需要做一次性的数据迁移:
-- Iceberg → DuckLake 迁移示例
-- 步骤 1:在 DuckLake 中创建相同结构的表
CREATE TABLE ducklake_users LIKE iceberg_db.users;
-- 步骤 2:从 Iceberg 读取并写入 DuckLake
INSERT INTO ducklake_users
SELECT * FROM iceberg_db.users;
-- 步骤 3:验证数据一致性
SELECT
'iceberg' as source,
COUNT(*) as row_count,
COUNT(DISTINCT user_id) as unique_users
FROM iceberg_db.users
UNION ALL
SELECT
'ducklake' as source,
COUNT(*) as row_count,
COUNT(DISTINCT user_id) as unique_users
FROM ducklake_users;
-- 步骤 4:切换应用层连接字符串(蓝绿部署)
-- 旧: iceberg:s3://.../events/
-- 新: ducklake:s3://.../events/
7.2 v1.0 之后的路线图
根据 DuckDB 官方博客透露的信息,DuckLake 的 roadmap 包括:
v1.1 (Q3 2026):
├── Trino/Presto 连接器
├── 更完善的 PostgreSQL 目录支持
└── 数据重分区 API
v1.2 (Q4 2026):
├── 跨云多活目录(Multi-region Catalog)
├── Change Data Feed (CDC 变更捕获)
└── 与 dbt 的原生集成
v2.0 (2027):
├── 完整的 ACID 隔离级别支持
├── 分布式 DuckDB 计算引擎
└── 自动查询优化(Adaptive Query Optimization)
八、SAP 收购 Dremio:湖仓格局的变局
就在 DuckLake v1.0 发布后不到一个月,2026 年 5 月 4 日,SAP 宣布收购开放式湖仓平台 Dremio。这一消息在数据圈引发了广泛讨论。
这意味着什么?
Dremio 是基于 Apache Arrow 和 Iceberg 的开源湖仓平台,在 GitHub 上有超过 9000 颗星,被大量企业用于构建开放数据湖。SAP 收购 Dremio 的战略意图非常明显:
- 对抗 Snowflake 和 Databricks:SAP 的 Business Technology Platform 需要一个开放、可扩展的数据底座,Dremio 的 Iceberg 原生支持使其成为最佳选择
- AI 时代的数据战略:AI Agent 需要访问企业数据,而 Dremio 的 Semantic Layer(语义层)可以将企业数据以一种 AI 可理解的格式暴露给 Agent
- 湖仓一体的企业化:从"数据湖开放"转向"数据湖企业化",SAP 看到了这个趋势的商业价值
DuckLake 的机会在哪里?
-- DuckLake 的定位恰好在 Dremio 和 Snowflake 之间:
-- Dremio: 企业级 Iceberg 湖仓(重型、功能全)
-- Snowflake: 数据仓库即服务(完全托管、贵)
-- DuckLake: 轻量级开放湖仓(低成本、SQL 原生、云无关)
-- DuckLake 的目标用户:
-- ✅ 预算有限的初创公司(需要开放数据湖)
-- ✅ 多云部署的企业(不想被单一云厂商锁定)
-- ✅ 数据工程师个人(需要本地开发和生产一致性)
-- ✅ AI 应用开发者(需要快速迭代数据管道)
SAP + Dremio 的组合代表了大企业的湖仓战略,而 DuckLake 则代表了一种更轻量、更开放、更开发者友好的路径。这两条路并非互斥——在大型组织中,它们可以共存:DuckLake 用于数据生产(写入),Dremio/Snowflake 用于数据分析(读取)。
九、总结与展望
9.1 DuckLake 的核心价值
DuckLake v1.0 是一次"正确的架构决策"带来的性能跃迁。它的核心价值不在于"又一个湖仓格式",而在于重新思考了元数据存储的位置:
| 创新点 | 技术影响 |
|---|---|
| 关系型数据库存储元数据 | 元数据查询从 O(n) 文件扫描 → O(log n) 索引查询 |
| 数据内联机制 | 小文件问题从"治标不治本" → 根本消除 |
| SQL 原生设计 | 不需要学习专有语法,降低使用门槛 |
| v1.0 生产就绪规范 | 提供向后兼容保证,企业可以放心使用 |
| 926x 查询性能提升 | 使实时交互式分析在数据湖上成为可能 |
9.2 给工程师的建议
如果你是数据平台工程师:
- 评估 DuckLake 作为新数据湖项目的首选格式
- 关注 DuckLake 与现有 Spark/Flink pipeline 的集成方案
- 定期跟进 v1.1+ 的 Trino 连接器和 PostgreSQL 目录增强
如果你是 AI 应用开发者:
- DuckLake + DuckDB 的组合是构建 AI 数据管道的利器
- 数据内联机制使得 AI 训练数据的实时更新变得简单
- 结合 DuckDB 的向量化执行引擎,可以实现毫秒级的交互式数据探索
如果你是数据库内核开发者:
- DuckLake 的架构证明了一个看似"显而易见"的决策(用数据库存元数据)能带来多大收益
- 关系型数据库在 OLAP 场景下的生命力被大大低估了
- 关注 DuckDB 团队如何继续演进这一架构
9.3 数据基础设施的未来
回顾数据基础设施的演进历程:
2010年前: 数据仓库时代(Teradata/Oracle → 锁定、昂贵)
2010-2017: Hadoop 时代(开源 → 便宜、复杂、数据沼泽)
2017-2023: 湖仓一体时代(Iceberg/Hudi/Delta → 开放、性能有限)
2026-: DuckLake 时代(关系型元数据 + 对象存储 → 开放、高性能、开发者友好)
DuckLake 代表着数据基础设施进入了一个新阶段:不需要在开放性和性能之间做妥协。这个转变的背后,是一个简单但深刻的设计洞察——元数据应该由最适合管理结构化数据的系统(关系型数据库)来管理,而不是用文件来模拟结构化数据。
附录:DuckLake 资源汇总
# DuckDB 安装
brew install duckdb
pip install "duckdb>=1.5.2"
# DuckLake 官方文档
https://duckdb.org/2026/04/13/announcing-duckdb-152
https://ducklake.select/
# DuckLake v1.0 规范
https://ducklake.select/2026/04/13/announcing-ducklake-v1/
# DuckLake 数据内联详解
https://ducklake.select/2026/04/02/data-inlining-in-ducklake/
# GitHub
https://github.com/duckdb/duckdb (DuckLake extension)
# 官方基准测试白皮书
https://duckdb.org/docs/ducklake/benchmarks
# DuckLake Discord 社区
https://discord.gg/duckdb
参考来源:
- DuckDB v1.5.2 发布公告: https://duckdb.org/2026/04/13/announcing-duckdb-152
- DuckLake v1.0 发布说明: https://ducklake.select/2026/04/13/announcing-ducklake-v1/
- DuckLake 数据内联机制: https://ducklake.select/2026/04/02/data-inlining-in-ducklake/
- 至顶网 - DuckDB 用关系型数据库攻克数据湖"小改动"难题: http://www.zhiding.cn/files/klist-0-256436-1.htm
- SAP 收购 Dremio: https://so.html5.qq.com/page/real/search_news?docid=70000021_59169f883d955452
- DuckLake SQL 原生表格式: https://blog.csdn.net/l1t/article/details/160727798
- AliSQL DuckDB 集成: https://tool.lu/article/7Fj/detail