编程 DuckLake v1.0 深度解析:DuckDB 团队如何用关系型数据库颠覆数据湖架构——926 倍性能背后的湖仓一体新范式

2026-05-09 13:14:28 +0800 CST views 2

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)。

小文件问题的后果是灾难性的:

  1. 元数据膨胀:720 个文件意味着元数据层需要追踪 720 个对象,每次列表操作都要扫描大量文件
  2. 查询性能恶化:大量小文件的读取导致频繁的 I/O 开销和网络请求
  3. 存储效率低下: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 数据文件                         │
│   • 只有数据,不含业务元数据                  │
└─────────────────────────────────────────────┘

关键洞察:存储层只关心"数据",目录层只关心"元数据"。职责分离带来了三大优势:

  1. 元数据查询不再是对象存储的负担:目录层是数据库,任何元数据查找都是毫秒级的索引查询
  2. 对象存储的负担大幅降低:对象存储只需要管理数据文件,不需要管理元数据文件
  3. 事务一致性由数据库保障:元数据的 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/DeltaDuckLake
小规模写入立即生成 Parquet → 小文件先内联到数据库 → 批量写入
写入延迟低(但后续问题多)极低(数据库写入比文件写入更快)
文件数量指数增长线性增长(由阈值控制)
查询性能小文件多时急剧下降始终稳定(大文件读取效率高)
压缩需求必须定期运行 Compaction可选(内联机制减少压缩压力)

2.4 与 Iceberg、Delta Lake 的技术对比

特性Apache IcebergDelta LakeApache HudiDuckLake
元数据存储JSON/Avro 文件Delta Log 文件Hoodie Metadata关系型数据库
小文件处理Compaction(额外开销)Auto CompactionClustering数据内联(原生)
查询性能随小文件增加下降随小文件增加下降中等始终稳定
生态系统Spark/Trino/FlinkSparkSpark/FlinkDuckDB 原生
标准化是(v1.0 规范发布)
事务支持
时间旅行
模式演进
SQL 标准部分部分部分完整 SQL 支持
基准查询速度基线~1.1x~1.2x926x

三、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 TABLEINSERTUPDATEDELETE

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 条/秒

结果令人震惊:

指标IcebergDuckLake提升倍数
端到端查询时间基准 (1x)926 倍926x
数据摄取吞吐量基准 (1x)105 倍105x
元数据查询延迟~500ms< 1ms500x
小文件数量(24h)~86,400~243600x
存储压缩频率每 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 数据库DuckDBSQL 查询引擎,无需服务端
生产数据湖(多租户、大规模)DuckLake元数据目录 + 对象存储,支持 ACID
Python DataFrame → 生产数据湖Polars + DuckLakePolars 处理数据,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';

这样做的好处是:

  1. 元数据完全独立于 DuckDB 实例
  2. PostgreSQL 的连接池、高可用(Patroni/pgpool)可以直接复用
  3. 团队已经熟悉的 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 的战略意图非常明显:

  1. 对抗 Snowflake 和 Databricks:SAP 的 Business Technology Platform 需要一个开放、可扩展的数据底座,Dremio 的 Iceberg 原生支持使其成为最佳选择
  2. AI 时代的数据战略:AI Agent 需要访问企业数据,而 Dremio 的 Semantic Layer(语义层)可以将企业数据以一种 AI 可理解的格式暴露给 Agent
  3. 湖仓一体的企业化:从"数据湖开放"转向"数据湖企业化",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

参考来源:

推荐文章

浅谈CSRF攻击
2024-11-18 09:45:14 +0800 CST
PHP 唯一卡号生成
2024-11-18 21:24:12 +0800 CST
PHP 的生成器,用过的都说好!
2024-11-18 04:43:02 +0800 CST
20个超实用的CSS动画库
2024-11-18 07:23:12 +0800 CST
2024年公司官方网站建设费用解析
2024-11-18 20:21:19 +0800 CST
设置mysql支持emoji表情
2024-11-17 04:59:45 +0800 CST
如何在Vue3中处理全局状态管理?
2024-11-18 19:25:59 +0800 CST
程序员茄子在线接单