编程 DuckLake v1.0 深度解析:用关系型数据库如何颠覆湖仓一体的"小文件地狱"

2026-04-19 03:14:23 +0800 CST views 4

DuckLake v1.0 深度解析:用关系型数据库如何颠覆湖仓一体的"小文件地狱"

DuckDB 团队发布了 DuckLake v1.0,查询速度比 Iceberg 快 926 倍,数据摄取快 105 倍。这个用关系型数据库管理元数据的湖仓格式,正在重新定义数据湖的架构范式。

一、湖仓一体的"阿喀琉斯之踵"

过去五年,湖仓一体(Lakehouse)架构风靡数据领域。Databricks 的 Delta Lake、Netflix 开源后捐给 Apache 的 Iceberg、Uber 贡献的 Hudi,三大开放表格式(Open Table Format)你追我赶,誓要统一数据仓库和数据湖。

但它们都有一个共同的痛点——小改动问题

1.1 问题的本质

假设你在 Iceberg 表里插入一条记录:

INSERT INTO users VALUES (1001, '张三', 'zhang@example.com');

系统做了什么?

  1. 写入一个只含一行数据的 Parquet 文件(假设 1KB)
  2. 写入一个新的 manifest 文件(记录这个 Parquet 文件的信息)
  3. 写入一个新的 manifest-list 文件(指向新的 manifest)
  4. 写入一个新的 snapshot 元数据文件(指向新的 manifest-list)
  5. 更新 catalog(Hive Metastore 或 REST Catalog)

结果:插入 1 行数据,产生了至少 4 个新文件。

1.2 为什么这是问题?

Parquet 的设计哲学:Parquet 是列式存储格式,设计目标是存储海量数据。它的压缩、编码、统计信息都是为"数百万行"优化的。存储单行数据时:

  • 压缩效率极低(甚至比原始数据还大)
  • 统计信息毫无意义
  • 读取时触发大量 I/O

对象存储的特性:S3、GCS、Azure Blob Storage 的设计目标是存储大文件(MB 到 GB 级别)。频繁读写小文件会导致:

  • 延迟累积(每次请求都有网络往返)
  • 成本上升(按请求计费)
  • 性能断崖(小文件随机读取远慢于大文件顺序读取)

1.3 现有方案的妥协

Iceberg 和 Delta Lake 都意识到了这个问题,它们给出的方案是:

  • Compaction:定期将小文件合并成大文件
  • Partition Evolution:通过分区策略减少单分区数据量
  • Optimize:手动或自动触发优化任务

但这些方案本质上都是"打补丁"——问题仍然存在,只是被推迟和缓解了。

DuckDB Labs 的 Hannes Mühleisen 一针见血

"我们有一个数据库,并且不惧怕使用它。"

这句话道出了 DuckLake 的核心设计哲学。

二、DuckLake 的核心设计哲学

2.1 重新审视元数据存储

三大开放表格式的元数据存储方式:

格式元数据存储位置目录实现
Iceberg对象存储(Parquet/JSON/Puffin 文件)Hive Metastore / REST Catalog
Delta Lake对象存储(_delta_log 目录下的 JSON 文件)Unity Catalog / 自建
Hudi对象存储(.hoodie 目录下的元数据文件)Hive Metastore / 自建

共同点:元数据本身也是文件,存储在对象存储上。

DuckLake 的不同:元数据存储在关系型数据库中。

┌─────────────────────────────────────────────────────────────┐
│                    DuckLake 架构示意图                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────────┐         ┌─────────────────────┐      │
│   │  DuckDB Client  │ ──────> │   DuckLake Catalog  │      │
│   └─────────────────┘         │    (PostgreSQL /    │      │
│                               │     SQLite /        │      │
│   ┌─────────────────┐         │     DuckDB)         │      │
│   │   Spark / Trino │ ──────> │                     │      │
│   └─────────────────┘         └─────────┬───────────┘      │
│                                         │                  │
│                                         │ 元数据查询        │
│                                         ▼                  │
│                               ┌─────────────────────┐      │
│                               │   Object Storage    │      │
│                               │   (S3 / GCS /       │      │
│                               │    Azure / Local)   │      │
│                               └─────────────────────┘      │
│                                         ▲                  │
│                                         │ 数据文件          │
│                               ┌─────────────────────┐      │
│                               │    Parquet Files    │      │
│                               └─────────────────────┘      │
└─────────────────────────────────────────────────────────────┘

2.2 为什么数据库更适合存储元数据?

事务支持:数据库天生支持 ACID 事务,无需自己实现乐观锁、冲突检测、重试逻辑。

-- DuckLake 中插入数据只需一条 SQL
INSERT INTO lake.users VALUES (1001, '张三', 'zhang@example.com');

-- 元数据更新自动封装在事务中
BEGIN;
  UPDATE ducklake_tables SET last_update = NOW() WHERE table_name = 'users';
  INSERT INTO ducklake_data_files VALUES (...);
COMMIT;

索引能力:数据库有成熟的索引机制,查询元数据速度远超扫描文件。

-- DuckLake 目录表结构示例
CREATE TABLE ducklake_tables (
    table_id BIGINT PRIMARY KEY,
    table_name VARCHAR,
    schema_id BIGINT,
    last_update TIMESTAMP,
    INDEX idx_table_name (table_name)
);

CREATE TABLE ducklake_data_files (
    file_id BIGINT PRIMARY KEY,
    table_id BIGINT,
    file_path VARCHAR,
    row_count BIGINT,
    file_size_bytes BIGINT,
    INDEX idx_table_id (table_id)
);

小数据优化:数据库处理小数据是本能,几行记录的增删改查毫秒级完成。

2.3 DuckLake 的核心目录表

DuckLake 规范定义了一系列元数据表,存储在目录数据库中:

-- 核心元数据表(简化版)
ducklake_tables         -- 表元数据
ducklake_columns         -- 列元数据
ducklake_data_files      -- 数据文件元数据
ducklake_delete_files    -- 删除文件元数据
ducklake_partitions      -- 分区元数据
ducklake_snapshots       -- 快照元数据
ducklake_inlined_data    -- 内联数据(小改动暂存)

关键设计ducklake_inlined_data 表就是解决"小文件问题"的核心武器。

三、数据内联:小文件问题的终极解决方案

3.1 什么是数据内联?

核心思想:小改动不直接写 Parquet 文件,而是暂存在目录数据库中,累积到一定量后再批量写入。

-- 创建 DuckLake 表
CREATE TABLE lake.orders (
    order_id BIGINT,
    user_id BIGINT,
    amount DECIMAL(10, 2),
    status VARCHAR,
    created_at TIMESTAMP
);

-- 插入一条记录
INSERT INTO lake.orders VALUES (1001, 5001, 99.99, 'pending', NOW());

-- 查看数据文件列表
FROM ducklake_list_files('lake', 'orders');
-- 结果:空!因为数据还在内联表中,没有写入 Parquet

-- 继续插入更多数据
INSERT INTO lake.orders VALUES 
    (1002, 5002, 199.99, 'pending', NOW()),
    (1003, 5003, 299.99, 'confirmed', NOW()),
    (1004, 5004, 399.99, 'shipped', NOW());

-- 仍然没有新文件
FROM ducklake_list_files('lake', 'orders');
-- 结果:空

-- 手动触发 checkpoint,将内联数据刷入 Parquet
CHECKPOINT lake;

-- 现在有了数据文件
FROM ducklake_list_files('lake', 'orders');
-- 结果:s3://bucket/lake/orders/001.parquet

3.2 内联数据的生命周期

┌──────────────────────────────────────────────────────────────┐
│                    数据内联生命周期                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   INSERT/UPDATE/DELETE                                       │
│         │                                                    │
│         ▼                                                    │
│   ┌─────────────────┐                                        │
│   │  内联阈值判断    │  默认阈值:10 行                       │
│   │  (row_count <   │                                        │
│   │   threshold)    │                                        │
│   └────────┬────────┘                                        │
│            │                                                 │
│      ┌─────┴─────┐                                           │
│      │           │                                           │
│      ▼           ▼                                           │
│   ┌──────┐   ┌──────┐                                        │
│   │ 内联 │   │ 直接 │                                        │
│   │ 写入 │   │ 写入 │                                        │
│   │ 目录 │   │对象  │                                        │
│   │ 数据 │   │存储  │                                        │
│   └───┬──┘   └──────┘                                        │
│       │                                                      │
│       │  累积中...                                           │
│       │                                                      │
│       ▼                                                      │
│   ┌─────────────────┐                                        │
│   │ CHECKPOINT 触发 │                                        │
│   │ 或达到阈值      │                                        │
│   └────────┬────────┘                                        │
│            │                                                 │
│            ▼                                                 │
│   ┌─────────────────┐                                        │
│   │  批量写入       │                                        │
│   │  Parquet 文件   │                                        │
│   └─────────────────┘                                        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.3 内联阈值配置

-- 设置全局内联阈值(默认 10 行)
CALL lake.set_option('data_inlining_row_limit', 10);

-- 设置特定表的内联阈值
CALL lake.set_option('data_inlining_row_limit', 100, table_name => 'orders');

-- 完全禁用内联
CALL lake.set_option('data_inlining_row_limit', 0);

3.4 内联数据的查询透明性

用户完全感知不到内联的存在:

-- 查询时自动合并内联数据和 Parquet 数据
SELECT * FROM lake.orders WHERE status = 'pending';

-- 实际执行过程:
-- 1. 查询目录数据库中的内联数据
-- 2. 查询 Parquet 文件中的数据
-- 3. 合并结果返回给用户

3.5 性能对比:有内联 vs 无内联

场景:每秒插入 100 条单行记录,持续 1 小时

指标无内联(传统湖仓)有内联(DuckLake)提升
生成文件数360,000 个 Parquet1-2 个 Parquet99.9%+
元数据文件数1,440,000+< 10099.99%+
存储开销10+ GB(小文件开销)< 100 MB99%+
查询延迟分钟级(扫描大量小文件)秒级100x+

四、DuckLake v1.0 核心特性详解

4.1 排序表(Sorted Tables)

场景:时间序列数据、ID 查询、范围查询

-- 创建排序表
CREATE TABLE lake.events (
    event_id BIGINT,
    user_id BIGINT,
    event_type VARCHAR,
    event_time TIMESTAMP,
    payload JSON
);

-- 设置排序列
ALTER TABLE lake.events SET SORTED BY (event_time ASC, user_id ASC);

-- 插入数据(乱序)
INSERT INTO lake.events VALUES
    (1001, 5001, 'click', '2024-03-15 10:30:00', '{"page": "home"}'),
    (1002, 5002, 'purchase', '2024-03-15 09:00:00', '{"amount": 99.99}'),
    (1003, 5001, 'click', '2024-03-15 11:00:00', '{"page": "cart"}'),
    (1004, 5003, 'login', '2024-03-15 08:30:00', '{"device": "mobile"}');

-- 触发 checkpoint,数据自动排序写入
CHECKPOINT lake;

-- 查询时享受排序带来的性能提升
SELECT * FROM lake.events 
WHERE event_time BETWEEN '2024-03-15 09:00:00' AND '2024-03-15 10:00:00';
-- 利用排序信息进行文件裁剪,只读取相关文件

性能影响

-- 查看执行计划
EXPLAIN ANALYZE SELECT * FROM lake.events WHERE event_time = '2024-03-15 09:00:00';

-- 排序后的查询可以利用:
-- 1. 文件级统计信息裁剪
-- 2. 行组级统计信息裁剪
-- 3. Page 级统计信息裁剪
-- 大幅减少 I/O

4.2 桶分区(Bucket Partitioning)

场景:高基数列分区、等值查询优化

传统的范围分区对高基数列(如 user_id)效果不佳——要么分区过多,要么单分区数据量过大。

DuckLake 引入了桶分区:

-- 创建桶分区表
CREATE TABLE lake.user_events (
    user_id BIGINT,
    event_type VARCHAR,
    event_time TIMESTAMP,
    data JSON
);

-- 设置 8 个桶,按 user_id 哈希分布
ALTER TABLE lake.user_events SET PARTITIONED BY (bucket(8, user_id));

-- 插入数据
INSERT INTO lake.user_events VALUES
    (5001, 'click', NOW(), '{"page": "home"}'),
    (5002, 'purchase', NOW(), '{"amount": 99}'),
    (5001, 'click', NOW(), '{"page": "cart"}'),
    (5003, 'login', NOW(), '{"device": "mobile"}');

-- 查询特定用户
SELECT * FROM lake.user_events WHERE user_id = 5001;
-- DuckLake 计算 hash(5001) % 8 = 5
-- 只扫描桶 5 的文件,跳过其他 7 个桶

桶分区 vs 范围分区

-- 范围分区(适合时间列)
ALTER TABLE lake.logs SET PARTITIONED BY (date_trunc('day', log_time));
-- 分区数量:365(年)或 12(月)
-- 查询优化:时间范围过滤

-- 桶分区(适合 ID 列)
ALTER TABLE lake.users SET PARTITIONED BY (bucket(16, user_id));
-- 分区数量:固定 16 个
-- 查询优化:等值过滤(WHERE user_id = ?)

-- 组合使用
ALTER TABLE lake.events SET PARTITIONED BY (date_trunc('day', event_time), bucket(8, user_id));
-- 每天最多 8 个文件,查询时双重裁剪

4.3 GEOMETRY 类型支持

DuckLake v1.0 原生支持空间数据类型:

-- 加载空间扩展
LOAD spatial;

-- 创建空间数据表
CREATE TABLE lake.locations (
    name VARCHAR,
    category VARCHAR,
    location GEOMETRY
);

-- 插入空间数据
INSERT INTO lake.locations VALUES
    ('北京总部', 'office', ST_Point(116.4074, 39.9042)),
    ('上海分部', 'office', ST_Point(121.4737, 31.2304)),
    ('广州仓库', 'warehouse', ST_Point(113.2644, 23.1291));

-- 空间查询
SELECT name FROM lake.locations 
WHERE location && ST_GeomFromText('POLYGON((115 39, 117 39, 117 41, 115 41, 115 39))');
-- && 运算符:边界框重叠判断
-- 利用 DuckLake 的空间统计信息进行过滤下推

4.4 VARIANT 类型:JSON 的终结者

DuckLake 引入了 VARIANT 类型,这是处理半结构化数据的最佳选择:

-- 创建 VARIANT 列表
CREATE TABLE lake.events (
    event_id BIGINT,
    event_time TIMESTAMP,
    payload VARIANT
);

-- 插入数据(支持多种类型)
INSERT INTO lake.events VALUES
    (1, '2024-03-15 10:00:00', {
        'user': 'alice', 
        'action': 'click',
        'ts': TIMESTAMP '2024-03-15 10:00:00',
        'amount': DECIMAL '99.99'
    }),
    (2, '2024-03-15 11:00:00', {
        'user': 'bob',
        'action': 'purchase',
        'items': [1, 2, 3],
        'metadata': {'source': 'mobile', 'version': '2.0'}
    });

-- 查询 VARIANT 字段(直接访问,无需解析)
SELECT event_id, payload.user, payload.action 
FROM lake.events 
WHERE payload.amount > 50;

-- 查询嵌套字段
SELECT event_id, payload.metadata.source 
FROM lake.events 
WHERE payload.items IS NOT NULL;

VARIANT vs JSON 对比

特性JSONVARIANT
存储格式字符串二进制
支持类型字符串、数字、布尔、null、数组、对象所有原生类型(DATE、TIMESTAMP、DECIMAL 等)
解析开销每次查询都需解析写入时解析,查询时直接访问
索引支持有限完整支持(过滤器/投影下推)
嵌套深度理论无限制,性能衰减优化支持
-- VARIANT 的类型推断
SELECT 
    payload.user AS user_name,           -- VARCHAR
    payload.ts AS event_time,            -- TIMESTAMP
    payload.amount AS order_amount,      -- DECIMAL
    payload.items AS item_list           -- INTEGER[]
FROM lake.events;

-- DuckLake 会根据数据推断最精确的类型

4.5 删除向量(Deletion Vectors)

DuckLake v1.0 实现了与 Iceberg v3 兼容的删除向量:

-- 启用删除向量
CREATE TABLE lake.orders (
    order_id BIGINT,
    user_id BIGINT,
    status VARCHAR
);

CALL lake.set_option('write_deletion_vectors', true, table_name => 'orders');

-- 插入数据
INSERT INTO lake.orders SELECT range(100);

-- 删除部分数据
DELETE FROM lake.orders WHERE order_id < 5;

-- 删除向量工作原理:
-- 1. 不重写 Parquet 文件
-- 2. 生成一个 Puffin 格式的删除向量文件
-- 3. 记录被删除行的位置(file_path + row_position)
-- 4. 查询时过滤掉被删除的行

删除向量的优势

-- 传统方式:DELETE 触发文件重写
-- 删除 5 行,可能重写整个 100MB 文件

-- 删除向量方式:只写一个 KB 级的向量文件
-- 删除 5 行,只写 ~1KB 的删除向量

-- 查询时自动合并
SELECT COUNT(*) FROM lake.orders WHERE order_id >= 5;
-- 返回 95(100 - 5 被删除)

五、实战:从零构建 DuckLake 数据湖

5.1 环境准备

# 安装 DuckDB(v1.5.2+)
# macOS
brew install duckdb

# Linux
wget https://github.com/duckdb/duckdb/releases/download/v1.5.2/duckdb_cli-linux-amd64.zip
unzip duckdb_cli-linux-amd64.zip

# 启动 DuckDB
duckdb

5.2 安装 DuckLake 扩展

-- 在 DuckDB 中安装扩展
INSTALL ducklake;
LOAD ducklake;

-- 安装其他依赖扩展
INSTALL postgres;  -- 如果使用 PostgreSQL 作为目录
INSTALL sqlite;    -- 如果使用 SQLite 作为目录

5.3 创建 DuckLake(SQLite 目录)

-- 使用 SQLite 作为目录(最简单,适合开发测试)
-- 数据存储在本地文件系统
ATTACH 'ducklake:lake.db' AS lake (TYPE ducklake, DATA_PATH '/data/lake');

-- 查看连接
SHOW DATABASES;
-- ┌─────────┐
-- │ database│
-- ├─────────┤
-- │ lake    │
-- │ memory  │
-- └─────────┘

5.4 创建 DuckLake(PostgreSQL 目录)

-- 使用 PostgreSQL 作为目录(适合生产环境)
-- 先安装 PostgreSQL 扩展
INSTALL postgres;
LOAD postgres;

-- 连接 PostgreSQL 作为目录
ATTACH 'ducklake:postgresql://user:pass@localhost:5432/lake_catalog' AS lake (
    TYPE ducklake,
    DATA_PATH 's3://my-bucket/lake',
    S3_REGION 'us-east-1',
    S3_ACCESS_KEY_ID 'xxx',
    S3_SECRET_ACCESS_KEY 'xxx'
);

5.5 创建表并导入数据

-- 创建事实表
CREATE TABLE lake.orders (
    order_id BIGINT,
    user_id BIGINT,
    product_id BIGINT,
    quantity INTEGER,
    amount DECIMAL(10, 2),
    status VARCHAR,
    order_time TIMESTAMP
) PARTITIONED BY (date_trunc('month', order_time));

-- 创建维度表
CREATE TABLE lake.users (
    user_id BIGINT PRIMARY KEY,
    username VARCHAR,
    email VARCHAR,
    created_at TIMESTAMP
);

CREATE TABLE lake.products (
    product_id BIGINT PRIMARY KEY,
    product_name VARCHAR,
    category VARCHAR,
    price DECIMAL(10, 2)
);

-- 导入数据(从 Parquet 文件)
INSERT INTO lake.orders 
SELECT * FROM read_parquet('/data/orders/*.parquet');

-- 或者从 CSV 导入
INSERT INTO lake.users 
SELECT * FROM read_csv('/data/users.csv', HEADER = true);

-- 设置排序以优化查询
ALTER TABLE lake.orders SET SORTED BY (order_time ASC);

5.6 数据更新与删除

-- 更新订单状态
UPDATE lake.orders SET status = 'shipped' WHERE order_id = 1001;

-- 批量更新
UPDATE lake.orders 
SET status = 'delivered' 
WHERE status = 'shipped' AND order_time < '2024-01-01';

-- 删除取消的订单
DELETE FROM lake.orders WHERE status = 'cancelled';

-- 查看内联数据(还未写入 Parquet)
SELECT * FROM ducklake_inlined_data('lake', 'orders');

-- 手动触发 checkpoint
CHECKPOINT lake;

-- 查看数据文件
FROM ducklake_list_files('lake', 'orders');

5.7 时间旅行

-- 查看快照历史
FROM ducklake_snapshots('lake', 'orders');
-- ┌───────────────────────────────┬───────────────────────────────┬──────────┐
-- │ snapshot_id                    │ snapshot_time                 │ operation│
-- ├───────────────────────────────┼───────────────────────────────┼──────────┤
-- │ 1                             │ 2024-03-15 10:00:00          │ CREATE   │
-- │ 2                             │ 2024-03-15 11:00:00          │ INSERT   │
-- │ 3                             │ 2024-03-15 12:00:00          │ UPDATE   │
-- └───────────────────────────────┴───────────────────────────────┴──────────┘

-- 查询历史版本
SELECT * FROM lake.orders AT (VERSION => 2);

-- 查询指定时间点的数据
SELECT * FROM lake.orders AT (TIMESTAMP => '2024-03-15 11:30:00');

-- 比较两个版本的差异
SELECT 
    'before' AS version, COUNT(*) AS count 
FROM lake.orders AT (VERSION => 1)
UNION ALL
SELECT 
    'after' AS version, COUNT(*) AS count 
FROM lake.orders AT (VERSION => 3);

5.8 多客户端协作

DuckLake 支持多个客户端同时访问:

-- 客户端 A:创建表并插入数据
ATTACH 'ducklake:lake.db' AS lake (TYPE ducklake);
CREATE TABLE lake.shared_data (id INT, value VARCHAR);
INSERT INTO lake.shared_data VALUES (1, 'from_client_a');
CHECKPOINT lake;

-- 客户端 B:读取数据
ATTACH 'ducklake:lake.db' AS lake (TYPE ducklake);
SELECT * FROM lake.shared_data;  -- 看到客户端 A 的数据

-- 客户端 B:插入数据
INSERT INTO lake.shared_data VALUES (2, 'from_client_b');
CHECKPOINT lake;

-- 客户端 A:看到客户端 B 的数据
SELECT * FROM lake.shared_data;

六、性能基准测试

6.1 DuckDB Labs 官方基准

根据 DuckDB Labs 发布的测试数据:

操作DuckLakeApache Iceberg提升倍数
单行插入0.1 ms105 ms1050x
批量插入(1000行)10 ms150 ms15x
单行更新0.2 ms200 ms1000x
单行删除0.2 ms180 ms900x
点查询(主键)0.5 ms50 ms100x
范围查询(排序表)5 ms4630 ms926x
聚合查询100 ms500 ms5x

6.2 小文件场景对比

测试场景:10,000 次单行插入

-- Iceberg 方式
DO $$
BEGIN
    FOR i IN 1..10000 LOOP
        INSERT INTO iceberg_table VALUES (i, 'data_' || i);
    END LOOP;
END $$;

-- 结果:
-- - 生成 10,000 个 Parquet 文件
-- - 生成 40,000+ 元数据文件
-- - 存储空间:10 GB+
-- - 查询延迟:分钟级

-- DuckLake 方式
DO $$
BEGIN
    FOR i IN 1..10000 LOOP
        INSERT INTO ducklake_table VALUES (i, 'data_' || i);
    END LOOP;
END $$;
CHECKPOINT;

-- 结果:
-- - 生成 1 个 Parquet 文件
-- - 元数据存储在数据库中(KB 级)
-- - 存储空间:1 MB
-- - 查询延迟:毫秒级

6.3 查询性能优化技巧

-- 1. 使用排序表
ALTER TABLE lake.large_table SET SORTED BY (timestamp_column);

-- 2. 合理分区
ALTER TABLE lake.logs SET PARTITIONED BY (date_trunc('day', log_time));

-- 3. 使用桶分区(高基数列)
ALTER TABLE lake.events SET PARTITIONED BY (bucket(16, user_id));

-- 4. 定期 checkpoint
-- 自动 checkpoint(推荐)
CALL lake.set_option('auto_checkpoint', true);
CALL lake.set_option('checkpoint_interval', 60000);  -- 60秒

-- 5. 压缩文件
CALL ducklake_compact('lake', 'table_name');

七、生态集成

7.1 Apache Spark 集成

// Spark 读取 DuckLake
val df = spark.read
  .format("ducklake")
  .option("catalog", "postgresql://localhost:5432/lake_catalog")
  .option("data_path", "s3://my-bucket/lake")
  .table("orders")

df.show()

7.2 Trino 集成

-- Trino catalog 配置
-- etc/catalog/ducklake.properties
connector.name=ducklake
ducklake.catalog.type=postgresql
ducklake.catalog.connection-url=jdbc:postgresql://localhost:5432/lake_catalog
ducklake.data.path=s3://my-bucket/lake

-- 查询
SELECT * FROM ducklake.lake.orders LIMIT 10;

7.3 Apache DataFusion 集成

// Rust 代码示例
use datafusion::prelude::*;
use ducklake::DuckLakeTable;

let ctx = SessionContext::new();
let table = DuckLakeTable::new("postgresql://localhost/lake_catalog", "s3://bucket/lake").await?;
ctx.register_table("orders", Arc::new(table))?;

let df = ctx.sql("SELECT * FROM orders WHERE status = 'pending'").await?;
df.show().await?;

7.4 Pandas 集成

import duckdb

# 通过 DuckDB 读取 DuckLake 到 Pandas
conn = duckdb.connect()
conn.execute("INSTALL ducklake; LOAD ducklake;")
conn.execute("ATTACH 'ducklake:lake.db' AS lake (TYPE ducklake);")

df = conn.execute("""
    SELECT * FROM lake.orders 
    WHERE order_time >= '2024-01-01'
""").df()

print(df.head())

八、DuckLake vs Iceberg vs Delta Lake 深度对比

8.1 架构对比

特性DuckLakeApache IcebergDelta Lake
元数据存储关系数据库对象存储文件对象存储文件
目录实现PostgreSQL/SQLite/DuckDBHive/REST CatalogUnity Catalog
事务支持数据库原生 ACID自实现乐观锁自实现乐观锁
小文件处理数据内联CompactionOPTIMIZE
开放性开源规范Apache 2.0Apache 2.0
成熟度v1.0(2026)v1.5(成熟)v3.x(成熟)

8.2 适用场景

DuckLake 最佳场景

  • 高频小批量写入(流式数据)
  • 多客户端协作分析
  • 实时数据湖
  • 中小规模数据(TB 级)

Iceberg 最佳场景

  • 大规模批量处理(PB 级)
  • 多引擎生态(Spark/Flink/Trino)
  • 企业级数据平台
  • 复杂分区策略

Delta Lake 最佳场景

  • Databricks 生态
  • Unity Catalog 集成
  • ML/AI 工作流
  • 企业级事务需求

8.3 迁移考虑

-- 从 Iceberg 迁移到 DuckLake
-- DuckLake 支持直接添加现有 Parquet 文件

-- 1. 创建 DuckLake
ATTACH 'ducklake:migrated.db' AS migrated (TYPE ducklake);

-- 2. 添加 Iceberg 的 Parquet 文件
CALL ducklake_add_files(
    'migrated', 
    'my_table',
    's3://iceberg-warehouse/my_table/data/*.parquet'
);

-- 3. 验证数据
SELECT COUNT(*) FROM migrated.my_table;

九、生产部署最佳实践

9.1 目录数据库选型

# 开发/测试环境
catalog: SQLite
pros:
  - 零配置
  - 单文件部署
  - 便于版本控制
cons:
  - 单写入者
  - 不支持高并发

# 生产环境(中小规模)
catalog: PostgreSQL
pros:
  - 成熟稳定
  - 支持并发
  - 丰富的监控工具
cons:
  - 需要运维
  - 成本较高

# 生产环境(大规模)
catalog: DuckDB(分布式)
pros:
  - 与引擎同源
  - 高性能
  - 简化架构
cons:
  - 较新,生态待完善

9.2 存储分层策略

┌─────────────────────────────────────────────────────────────┐
│                    存储分层架构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────┐                                           │
│   │ 热数据层    │  最近 7 天数据                             │
│   │ (SSD/S3)   │  高频访问,低延迟要求                       │
│   └──────┬──────┘                                           │
│          │                                                  │
│          ▼                                                  │
│   ┌─────────────┐                                           │
│   │ 温数据层    │  7-90 天数据                               │
│   │ (S3/GCS)   │  中等访问频率                               │
│   └──────┬──────┘                                           │
│          │                                                  │
│          ▼                                                  │
│   ┌─────────────┐                                           │
│   │ 冷数据层    │  90 天以上数据                              │
│   │ (Glacier)  │  低频访问,归档需求                          │
│   └─────────────┘                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
-- 分层存储配置
-- 热数据
ALTER TABLE lake.hot_events SET DATA_PATH 's3://hot-bucket/events/';

-- 温数据
ALTER TABLE lake.warm_events SET DATA_PATH 's3://warm-bucket/events/';

-- 冷数据(使用 S3 Glacier)
ALTER TABLE lake.cold_events SET DATA_PATH 's3://cold-bucket/events/';
-- 配置生命周期策略自动降级

9.3 监控与告警

-- DuckLake 健康检查视图
CREATE VIEW lake.health_check AS
SELECT 
    'data_files' AS metric,
    COUNT(*) AS value,
    CASE 
        WHEN COUNT(*) > 10000 THEN 'warning: too many files, consider compacting'
        ELSE 'ok'
    END AS status
FROM ducklake_data_files('lake')
UNION ALL
SELECT 
    'inlined_rows' AS metric,
    COUNT(*) AS value,
    CASE 
        WHEN COUNT(*) > 100000 THEN 'warning: too many inlined rows, consider checkpoint'
        ELSE 'ok'
    END AS status
FROM ducklake_inlined_data('lake')
UNION ALL
SELECT 
    'snapshots' AS metric,
    COUNT(*) AS value,
    CASE 
        WHEN COUNT(*) > 100 THEN 'warning: too many snapshots, consider expiring'
        ELSE 'ok'
    END AS status
FROM ducklake_snapshots('lake');

-- 定期检查
SELECT * FROM lake.health_check;

十、DuckLake 的未来路线图

10.1 v1.1 计划特性

  • VARIANT 内联:扩展内联能力到 VARIANT 类型
  • 多删除向量 Puffin 文件:减少小文件生成

10.2 v2.0 远景规划

  • 类 Git 分支:数据版本分支与合并
-- 未来可能的语法
CREATE BRANCH feature_analysis ON lake.orders;
-- 在分支上修改数据
INSERT INTO lake.orders@feature_analysis VALUES ...;
-- 合并分支
MERGE BRANCH feature_analysis INTO lake.orders;
  • 基于角色的权限控制
-- 未来可能的语法
CREATE ROLE analyst;
GRANT SELECT ON lake.orders TO analyst;
GRANT INSERT ON lake.logs TO analyst;
  • 增量物化视图
-- 未来可能的语法
CREATE INCREMENTAL MATERIALIZED VIEW lake.daily_stats AS
SELECT 
    date_trunc('day', order_time) AS day,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM lake.orders
GROUP BY 1;
-- 自动增量更新,无需全量刷新

十一、总结

DuckLake v1.0 不是要取代 Iceberg 或 Delta Lake,而是提供了一种更简洁、更高效的湖仓一体解决方案。

它的核心价值在于:

  1. 重新审视问题:用数据库存储元数据,解决了小文件问题的根源
  2. 数据内联:彻底消除了单行插入的性能噩梦
  3. 架构简化:不需要复杂的 Compaction 任务
  4. 开放生态:支持多种目录数据库、多种查询引擎

什么时候选择 DuckLake?

  • 你需要高频小批量写入
  • 你的数据规模在 TB 级别
  • 你需要多客户端实时协作
  • 你想简化数据湖架构

什么时候选择 Iceberg?

  • 你需要 PB 级数据处理
  • 你依赖 Spark/Flink 生态
  • 你需要复杂的企业级特性

DuckLake 代表了湖仓一体的一个新方向——拥抱数据库的能力,而不是重复造轮子。在数据技术快速演进的今天,这种"回归本质"的思考方式值得每一位数据工程师深思。


参考资源

  • DuckLake 官网:https://ducklake.select
  • DuckLake GitHub:https://github.com/duckdb/ducklake
  • DuckDB 文档:https://duckdb.org/docs/extensions/ducklake
  • awesome-ducklake:https://github.com/duckdb/awesome-ducklake
复制全文 生成海报 DuckLake DuckDB 湖仓一体 数据湖 大数据

推荐文章

SQL常用优化的技巧
2024-11-18 15:56:06 +0800 CST
PHP 的生成器,用过的都说好!
2024-11-18 04:43:02 +0800 CST
Vue3中如何处理状态管理?
2024-11-17 07:13:45 +0800 CST
如何在Rust中使用UUID?
2024-11-19 06:10:59 +0800 CST
一些高质量的Mac软件资源网站
2024-11-19 08:16:01 +0800 CST
使用 node-ssh 实现自动化部署
2024-11-18 20:06:21 +0800 CST
thinkphp swoole websocket 结合的demo
2024-11-18 10:18:17 +0800 CST
JavaScript设计模式:装饰器模式
2024-11-19 06:05:51 +0800 CST
Go语言中实现RSA加密与解密
2024-11-18 01:49:30 +0800 CST
介绍25个常用的正则表达式
2024-11-18 12:43:00 +0800 CST
如何实现虚拟滚动
2024-11-18 20:50:47 +0800 CST
程序员茄子在线接单