DB::DuckDB — HANDOFF

HANDOFF — DB::DuckDB 0.9.1

交接对象:接手维护者。本文所有数字为本机实测,出处见 duckdb-perf-next-steps.md 与 CHANGES.md。 交接时点:2026-10-29,提交 f66db4e,工作区干净。 0.7.2 补记:§8.3 的 README 过时描述与 stamp-file / abi-stamp 死代码已清理 (提交 f22811a),Windows 13 测试与 WSL zef install 双平台复核通过。 0.8.0 补记:§8.1 的 Parquet 直读写已落地(DB::DuckDB::Parquet + 驱动/连接方法), 实测读 Parquet 与读表同速、写 1M×4 列 0.033 s,详见 §8.1 与 CHANGES 0.8.0。 0.8.1 补记:§8.4 的 Arrow 无 shim 释放泄漏已修(缓存 callable,+69 MB → +2 MB), 并补 DB_DUCKDB_NO_ARROW_SHIM=1。 0.8.2 补记:§8.4 又补两处——无编译器时「回落」原本会抛异常(已在调用前探测库)、 Arrow shim 构建补齐并发加固(私有名 + per-file ABI 戳);另清 5 处注释乱码。 0.8.3 补记:§8.5 的缓存卫生做成 DB::DuckDB::ShimCache 统一清理(实测 8 个文件 → 4 个全活); Bridge 的两处同类问题一并修掉——mtime 判断换成 FNV-1a 内容指纹、构建加并发加固。 0.9.0 补记:新增 DB::DuckDB::TextColumn 零拷贝文本列视图(column-view($i).text), VARCHAR/BLOB 不再逐值造 Str,而是给出连续字节缓冲 + offset/length/NULL 三张表(详见 §8.6)。 shim ABI 由 5 抬到 6:新增 dkd_varchar_unpack_base(..., offset_base),旧符号保留为 base=0 包装。 实测 1M×VARCHAR(40) 构建 156 ms(to-list 仍 1535 ms,差距在 Raku 侧装箱); 标签 v0.9.0 已推 origin。 0.9.1 补记:三件事——ColumnView.at(:typed) / to-list(:typed) 把 DATE / TIMESTAMP 直接建成 Date / DateTime(不再先渲染文本再解析,见 §8.7);TextColumn.to-list 改整窗 解码 ASCII(1M×40 B 3.91 s → 1.91 s);execute-many 让带 ON CONFLICT 的批次走 TEMP 暂存表

  • 一条 INSERT ... SELECT 回放(20k 行 55.6 s → 0.25 s,见 §8.8)。

0. 三十秒版本

一个 Raku 的 DuckDB 驱动(NativeCall 绑定 libduckdb 2.x,实现 DB role)。 当前 0.9.1 的状态:读写两侧都有绕开逐值装箱的出口——VARCHAR / 时间 / DECIMAL / ENUM 四类值 都有 C 通道从原生载荷直接渲染;Parquet 读写已在 0.8.0 接上(走 SQL 层,见 §8.1); Arrow 无 shim 路径的释放泄漏在 0.8.1 修掉(§8.4);三个 shim 的构建与缓存卫生在 0.8.2/0.8.3 统一(§8.5); 0.9.0 新增 TextColumn 零拷贝文本列(§8.6),需要吞吐时改走 .text; 0.9.1 补上时序的 :typed 值(§8.7)与 ON CONFLICT 批次的暂存写入(§8.8)。

  • 16 个测试文件(514 个断言),Windows 与 WSL Ubuntu 双平台全绿

  • 无 C 编译器时全线回落到纯 Raku(含 TextColumn 的逐值兜底),功能不变、只是慢

  • 遗留:逐值取文本仍是 1.26 µs/值;批量读已由 .text 绕开(§8.2、§8.6)

1. 环境

Windows(主开发环境)

项目值
OSWindows 11
RakudoRakudo Star 2026.09,C:\rakudo\bin\raku.exe
libduckdbv2.0.0-dev1(Alien::Libduckdb:ver<1.1.0> 自编译产物)
C 编译器gcc(MSVC 亦可用,但 __int128 档只在 GCC/Clang 下生效)
仓库C:\Users\Admin\DB-DuckDB,remote [email protected]:skyter10086/DB-DuckDB.git,分支 master

WSL Ubuntu(第二验证环境,0.7.1 才打通)

项目值
发行版Ubuntu(WSL2),用户 zby,HOME /home/zby
Rakudorakubrew 装的 2026.09,不在默认 PATH:/home/zby/.rakubrew/versions/moar-2026.09/install/bin
zef/home/zby/.local/bin/zef
C 编译器/usr/bin/cc、gcc
Windows 仓库挂载/mnt/c/Users/Admin/DB-DuckDB

注意:~/.rakubrew/versions/moar-2026.09 是源码构建目录,bin/ 不存在;rakudo 可执行文件在 install/bin/ 下。用 rakubrew init 在当前环境里没生效,直接走绝对路径最省事。

2. 仓库结构

lib/DB/DuckDB.rakumod              驱动本体,does DB
lib/DB/DuckDB/Native.rakumod       libduckdb 全部 C 符号绑定(含 duckdb_result 固定布局结构体)
lib/DB/DuckDB/Bridge.rakumod       仅用于两个「按值传 duckdb_result」的访问器,改传指针绕过
lib/DB/DuckDB/Connection.rakumod   连接、prepared 缓存、事务
lib/DB/DuckDB/Statement.rakumod    占位符绑定(? 与 $name;DuckDB 不支持 :name)
lib/DB/DuckDB/Result.rakumod       结果读取总调度:解码路径选择、shim 探测、纯 Raku 回落
lib/DB/DuckDB/Decoder.rakumod      逐值通用解码器(所有回落路径的终点)
lib/DB/DuckDB/Parquet.rakumod      Parquet 读写的 SQL 构造:路径绑定、字面量/标识符转义、选项白名单
lib/DB/DuckDB/ShimCache.rakumod    三个 shim 共用的缓存清理(.bak / 孤儿戳 / 过期私有构建)
lib/DB/DuckDB/Chunk.rakumod        chunk 包装、生命周期
lib/DB/DuckDB/ColumnView.rakumod   零装箱列视图(0.6.8 起是真惰性)
lib/DB/DuckDB/TextColumn.rakumod   零拷贝文本列视图(0.9.0,字节缓冲 + offset/length/NULL 表)
lib/DB/DuckDB/Appender.rakumod     批量写入
lib/DB/DuckDB/VarcharShim.rakumod  VARCHAR/时间/DECIMAL shim 的构建、缓存、ABI 校验
lib/DB/DuckDB/VarcharShim/Native.rakumod  shim 的 NativeCall 声明(join-varchar / join-temporal / join-decimal)
lib/DB/DuckDB/Arrow.rakumod        Arrow C data interface 只读视图 + memcpy 绑定
lib/DB/DuckDB/ArrowShim.rakumod    Arrow 释放回调用的小 shim(Windows 上必需)
resources/duckdb-varchar-shim.c    自包含 C:string_t 布局解包 + 时间渲染 + DECIMAL 渲染
resources/duckdb-arrow-shim.c      Arrow release 回调
resources/duckdb-result-shim.c     by-value 结果结构的辅助
tools/perf/*.raku|*.ps1            基准与探针(见 §9)
t/01..16-*.rakutest                测试,16 个,可单独 raku 运行

3. 版本演进(只列对性能有实质影响的)

版本内容关键数字
0.6.0列式读取 + Appender 填充 + 并行解码雏形读路径从逐值解码改为 chunk/vector
0.6.1column-arrays(:parallel)按列并行解码
0.6.4惰性 column-views、点查修复未读列零成本
0.6.5VARCHAR 走 join+split 缓冲column() 2.7–3.4×
0.6.6Arrow C data interface 视图;修 chunk 所有权泄漏修复每轮约 23 MB 泄漏(200k 行 × 多轮,累计 4.9 GB 量级)
0.6.7按列权重切分并行、流式双缓冲预取首块可用 28 ms → 7.4 ms(1M×4 列)
0.6.8真惰性视图;META6 补 provides/resources、去 BOM—
0.6.9DATE/TIME/TIMESTAMP 从整数载荷渲染(shim ABI 4)DATE 8.38→1.38、TIMESTAMP 23.35→1.22、TIME 4.58→1.27 µs/值
0.7.0DECIMAL 在 C 里渲染、ENUM 查字典(ABI 5)DECIMAL(9,2) 2.45→1.23、(18,4) 3.52→1.24、(38,10) 9.42→1.48、ENUM 2.71→0.61 µs/值
0.7.1修 Linux 安装(msvcrt/libc 依赖误判)、shim 并发构建加固见 §7.10–7.12
0.7.2README 与代码对齐、清 stamp-file / abi-stamp 死代码—
0.8.0Parquet 读写在 SQL 层落地(DB::DuckDB::Parquet)写 1M×4 列 0.033 s;读与表扫描同速
0.8.1Arrow 无 shim 释放不再每次造 call stub60 轮 +69 MB → +2 MB
0.8.2Arrow 回落真实化、Arrow shim 并发加固6 进程并发首建 6/6 通过
0.8.3ShimCache 统一缓存清理;Bridge 改 FNV-1a 内容指纹缓存 8 个文件(4 死)→ 4 个全活
0.9.0TextColumn 零拷贝文本列(shim ABI 6,新增 offset_base)1M 值构建 156 ms;to-list 1.9 s
0.9.1时序 :typed 值、to-list 整窗解码、execute-many 的 ON CONFLICT 暂存to-list 3.91→1.91 s;20k upsert 55.6 s→0.25 s

4. 性能基线(0.7.1,1M 行,单进程单模式,best-of 3)

4.1 列读取(µs/值)

类型当前说明
BIGINT0.8–1.0to-list 类型特化后 1.71 s → 0.97 s
VARCHAR short(2 字符)0.95–0.96惰性视图与 eager 持平
VARCHAR long(91 字符)1.25–1.33视图反而快 6%
VARCHAR 中文(5 字符)1.05–1.13视图快 7%
DATE1.38参照 ::VARCHAR 1.30
TIMESTAMP1.22—
TIME1.27—
DECIMAL(9,2)1.23参照 1.27
DECIMAL(18,4)1.24参照 1.24
DECIMAL(38,10)1.48参照 1.42
ENUM0.61比 VARCHAR 通道快一倍,零分配

多列(µs/行):时间三列 .arrays 4.78(原 35.88);DECIMAL+ENUM 四列 .arrays 8.28(原 18.3–22)。

4.2 语句与写入

场景数字
SELECT 1 × 500(仅 execute + destroy)0.090 ms/次
prepared 复用 + bind + execute(纯 C)0.387 ms/次
现有 Raku 层 prepared(含 Result + 解码)0.486 ms/次
Appender 写 10 万行 × 4 列330 ms
多值 INSERT(1000 行/句,事务内)6 453 ms(19.5× 慢于 Appender)
execute-many + ON CONFLICT,20k 行(暂存路径)0.25 s(逐行 prepared 55.6 s,226×)
同上,100k 行0.69 s(逐行 prepared 245.7 s)

结论:语句固定成本的大头在 execute 本身,不在 prepare;Raku 层相对纯 C 只多 0.07–0.10 ms/语句。 单行点查不需要再优化,逐值 .at() 的 8.7 µs/值只影响大循环遍历,那种场景应改用 .chunks / to-list。

5. 架构要点(改代码前必读)

  1. duckdb_result 按固定 48 字节结构体映射(LP64:3 个 idx_t + 3 指针),加载时断言尺寸。 ABI 变了要立刻炸,而不是悄悄踩内存。

  2. 结果只走 chunk/vector API。duckdb_result_chunk_count / duckdb_fetch_chunk 拿 duckdb_data_chunk,直接读 vector 数据缓冲。不要用 CStruct 数组元素访问——NativeCall 在 Windows 上会 fault,一律用 uint32/uint64 视图逐字段解剖。

  3. 禁止按值返回/接收结构体,唯一例外是那两个仍声明 by-value duckdb_result 的访问器, 走 DB::DuckDB::Bridge 把同样 48 字节当指针传。NativeCall 的 by-value marshalling 在 MS x64 碰巧能跑,在 SysV 下会被误编译(表现为 DuckDB 内部 unique_ptr NULL 崩溃)。

  4. 值的文本与 DuckDB 自己的 CAST(x AS VARCHAR) 逐字节一致,不转 Raku Date/DateTime/Rat。 精确性优先,需要结构化时在调用点自己转。

  5. shim 全程可选。没编译器就回落纯 Raku,同一份数据两条路的 digest 必须相等(测试里逐字节比对)。 DB::DuckDB::Result 的默认 abi=0,只有 shim 真正可用才把 ABI 抬上去。

  6. prepared 语句按连接缓存,连接销毁时一并销毁;close 兜底清理调用者忘 finish 的连接, 否则 DuckDB 不会释放文件锁。

  7. Alien 依赖是钉死的(Alien::Libduckdb:ver<1.1.0> = libduckdb 2.0.0)。C API 跨大版本不 ABI 稳定(枚举重编号、访问器改按值传)。升级流程:读 duckdb.h → 核对 result 结构布局 → 跑 t/ → 再 bump pin。运行时还有第二道防线:加载后校验 major 版本,非 2.x 直接拒绝启动。

6. 已否决的方案(不要再花时间)

候选结论依据
列视图"20× 提升"概念错误真实收益是「未读列零成本」,不是 20×;读到的列仍是 0.8–3.2 µs/值
VARCHAR 跨段合并解码否决想回收的 16% 在 0.6.4 换实现后已消失,现差异 +1% 到 −7%;合并要先 memcpy 91 MB/列,净负
惰性 views 内存缓升(怀疑泄漏)非泄漏,不优化200k×3 列 200 轮外部采样:斜率逐段衰减、后段 r²≈0,平台不到 1 GB;三列 98 块若真漏早就 GB 级。是 Rakudo GC 高水位 + DuckDB 延迟回收
弃用 DB role 自写 API否决点查场景 15–20%,其余 <5%;role 是薄壳,真正决定性能的是 API 形态(列式/流式/零拷贝),保留 role 也能加
去装箱(弃用 Raku 装箱类型)伪命题装箱不是瓶颈;瓶颈是逐值方法调用与字符串构造
nqp:: 下沉热循环仅可选收益不抵可读性损失,且 nqp::collectgarbage 在 Rakudo 2026.09 已不存在
RakuAST 优化无用只作用于编译期,与运行期解码无关
DECIMAL 用纯 Raku 定点渲染搬进 C纯 Raku 单值格式化 1.71 µs > DuckDB 自己格式化 1.19 µs;在 Raku 里逐值拼十进制文本注定输
写入路径继续压(P3)主动搁置收益不抵复杂度,Appender 已比多值 INSERT 快 19.5×
VARCHAR 批量造 Str暂不做(0.9.0 绕开)判定线是 <0.4 µs/值,实测未达;改用 TextColumn 提供不走 Str 的出口(§8.6)
TextColumn 构建多线程化否决(0.9.2 实测后回退)MoarVM 每进程只跑一个 NativeCall,8 线程最多 1.1×,窄载荷反而更慢;详见 §8.11

7. 踩过的坑(复现过一次就别再踩)

  1. kind 从 0 起 → 分支不进:0 在 Raku 为假,elsif ... -> $kind 直接跳过,DATE 悄悄回落逐值解码。

  2. C 的整数除法向零截断:1970 年前的秒数会差一天。shim 里统一用 dkd_floor_div 向负无穷取整。

  3. DECIMAL(9,9) 打印 .5 而非 0.5:判据是 width == scale。DuckDB 没写进文档,是拿 ::VARCHAR 逐类型比出来的;C 与 Raku 两条路都要有这条判断。

  4. hugeint 负数低位为正时算错:高字 -1 只在低位也为负时才是 int64 符号扩展; 否则 -10^19 会读成 8446744073709551616。

  5. 文本格式四处不是「补零就完事」:公元前是 0001-12-31 (BC);24:00:00 合法; 小数部分去尾零(01:02:03.000400 → 01:02:03.0004);TIMESTAMP 最多 6 位、TIMESTAMP_NS 最多 9 位。

  6. TIMESTAMP_TZ / TIME_TZ 不能从 payload 复现:文本含会话时区,只能走 DuckDB 自己渲染 (TIMEZONE 变更后要刷新偏移)。

  7. Windows 上 libduckdb 实际 dll 名是 duckdb.dll,且不需要 MSVC .lib(运行时 LoadLibrary)。

  8. 跨线程 Hash 会丢键:并行解码时共享 Hash 必须在主线程预建。

  9. PowerShell 的 CLIXML 噪声:从 Raku 拉 PowerShell 子进程会把父控制台打成一堆 XML。 取进程 RSS 之类要外部采样(见 tools/perf/probe-views-rss.ps1),命令输出用 cmd /c + Where-Object 过滤。

  10. zef / raku-pm 会扫描源码里的 is native('...') 字面量,全部当硬依赖。写 is native('msvcrt') → Linux 上装不上(缺 msvcrt);写 is native('libc') → Windows 上装不上(缺 libc)。 正确做法:名字用常量拼出来,例如 my constant CLIB = $*DISTRO.is-win ?? 'msvc' ~ 'rt' !! 'libc' ~ '.so.6';

  11. NativeCall 会自动给裸名字加 lib 前缀:裸 libc 变成找 liblibc.so(不存在)。 带点的名字(如 libc.so.6)才按原样使用。

  12. .modified 在本 Rakudo 对刚写入的文件可能返回负 Instant,拿它判新旧会误把新库当陈旧库删掉。 缓存过期判断改用文件名里编码的 epoch 秒(lib-epoch)。

  13. 共享 shim 库被进程映射时 unlink / rename 会失败(Windows 更明显)。这不是 bug, 驱动器会自动落到私有名继续工作;缓存复用实测 306 ms 命中、13 次测试只留一份库。

8. 未完成事项(按 ROI 排序)

8.1 Parquet 直读写 —— 0.8.0 已完成

duckdb.h(2.0)里没有任何 parquet 符号,所以不存在可绑的 C 入口,"直"只能落在 SQL: 读走 read_parquet(...) 表函数,写走 COPY ... TO ... (FORMAT PARQUET)。新增 DB::DuckDB::Parquet 负责构造 SQL、转义与白名单校验,驱动与连接各挂 read-parquet / write-parquet / parquet-metadata / parquet-schema / parquet-row-groups。

关键设计(改代码前必读):

  • 读侧路径全部绑定(多文件是 read_parquet([?, ?]),一个占位符一路径),所以带单引号的 路径不需要转义;写侧必须插值目标路径与表名,走 parquet-literal / parquet-ident。

  • 读 Parquet 不是第二条解码路径:走同一套 chunk/vector,VARCHAR/时间/DECIMAL 的 C 通道自动生效。

  • row_group_size 只在 2048 行 vector 边界生效,实际行组是它的整数倍。

  • 分区目录读取时分区列不在文件里,hive_partitioning 把它追加在最后,按列序号读会踩到。

实测(1M 行 × 4 列,best-of-3,单进程,zstd):写 0.033 s / 2.05 MiB;读 BIGINT 0.58 s (表扫描 0.58)、VARCHAR 1.15(表 1.20)、四列 5.08 µs/行(表 5.05)、首块 19.8 ms (表 37.7);parquet-metadata 只读 footer,0.6 ms。对比 execute-many 的 11.3 µs/行, COPY 写每行快约 400 倍。

8.2 VARCHAR 批量造 Str —— 判定线未达(0.9.0 从另一侧绕开,见 §8.6)

判定条件是 <0.4 µs/值,当前 0.95–1.33。要再往下只能减少逐值 substr(已确认瓶颈在 Raku substr, 不在 C shim)。没想清楚之前不要动——需要吞吐的调用点改走 column-view($i).text,不造 Str。

8.3 README 与代码不同步(0.7.2 已解决)

  • ## Status 仍写 Not implemented (yet): … Appender —— 实际 Appender 早已实现并有 t/05-appender.rakutest

  • ## Requirements 写 tested on Rakudo 2025.06, Windows 11 —— 实际双平台 2026.09 验证通过

  • 顺手可清:VarcharShim.rakumod 里 stamp-file / abi-stamp 已无调用方

8.4 Arrow 在「无 shim」路径上的释放 —— 0.8.1 已修,0.8.2 收尾

根因不是所有权语义,而是 nativecast(:(Pointer), $ptr) 每次都会造一个新的 native call stub,且 VM 不回收。实测:release 指针在所有 chunk 上是同一个(245 个 chunk 只有 1 个不同 值),所以按指针值缓存 Code 即可,一次 cast 终身复用;Lock 只保护首次填充。

对比(60 轮 × 5 万行,约 1500 次释放):修前 +69 MB,修后 +2 MB(GC 噪声),有 shim 时 +5 MB。 同时补了 DB_DUCKDB_NO_ARROW_SHIM=1(此前 Arrow 没有对应 VARCHAR 的禁用开关), t/11 增加 2 条断言专测这条路径,否则它只能靠「机器上没有编译器」偶然触发。

0.8.2 又补了两处(都是查遗留时发现的,不是新功能):

  1. 所谓「无编译器回落」原本不成立:require 只能证明模块能编译,is native(&shim-library) 是惰性解析,所以真正的失败被推迟到第一次 native 调用——那时数组已在手上、没有第二条释放路。 现在先问 shim 模块「库到底存不存在」再决定用不用。验证方法:在库该在的位置放一个同名目录 让编译必然失败,t/11 在这条路上仍然 17/17 通过而不是抛异常。

  2. Arrow shim 的构建补齐了 0.7.1 给 VARCHAR shim 做过的并发加固:编译产物先落到私有名 libduckdb-arrow-shim-<pid>-<epoch>.<ext>,共享名空闲时才 rename;ABI 戳改成 per-file (<lib>.abi)而非目录级。6 进程并发首建 6/6 通过。

8.5 shim 缓存目录清理 —— 0.8.3 已系统解决

三个 shim 各自经历过命名方案变更,缓存目录里会积累再也没人读的文件:.bak、库已消失的 戳(含旧的目录级 libduckdb-x-shim.abi)、共享名被占用时留下的私有名构建。

现在统一由 DB::DuckDB::ShimCache::sweep 在每个进程解析到 shim 后清一次,三条保守规则: 有可用库才清(不会把缓存清空)、不碰当前解析到的库、可 DB_DUCKDB_NO_CACHE_SWEEP=1 关闭。 实测真实缓存 8 个文件(4 个是死的)→ 一次查询后 4 个,全部在用。t/15-shim-cache.rakutest 覆盖。

顺带修掉 Bridge 的两处同类问题(它此前是三个 shim 里唯一没加固过的):

  • 用 mtime 判断缓存是否新鲜 → 这个 Rakudo 对刚写入的文件返回负 Instant,导致桥接库 每次运行都重编译。改成 C 源内容的 FNV-1a 指纹(sha1 不在核心,属 Digest::SHA1)。

  • 直接链接到共享名 → 改成私有名 + rename,6 进程并发首建 6/6 通过。

8.6 零拷贝文本列 TextColumn —— 0.9.0 已完成

动机:column() / to-list 每个值都要在 Raku 里造一个 Str(VARCHAR 1.26 µs/值), 这就是与 Python fetchnumpy 的差距所在(1M 行 1240 ms vs 68 ms),而瓶颈在 Raku 侧 装箱与 substr,不在 C 绑定。0.9.0 给出一条完全不造 Str 的出口: $result.column-view($i).text(仅 VARCHAR / BLOB)返回 DB::DuckDB::TextColumn,内含

  • 一段连续的字节缓冲:bytes / byte-size,bytes-at($i) 直接切 Buf 视图;

  • 三张并行的表:offset($i) / length($i) / is-null($i),也可整张拿 offset-buffer / length-buffer / null-buffer 自己做批处理(例如交给 C 或 NQP 层);

  • at($i) / to-list 是便利出口,会造 Str,慢(见下),只用于校验与小结果。 0.9.1 起 to-list 把一个窗口内的 ASCII 载荷整块 decode 后按 substr 切片,只在窗口 非纯 ASCII(chars < 字节数)时回落到逐行 subbuf + decode;窗口上限 8 MiB, DB_DUCKDB_NO_TEXT_WHOLE=1 可强制旧路径。

实现要点(改之前必读):

  • 复用 resources/duckdb-varchar-shim.c 的解包循环,新增 dkd_varchar_unpack_base(data, validity, n, out, cap, offsets, lens, nulls, offset_base); 旧的 dkd_varchar_unpack(...) 保留为 offset_base = 0 的包装,DKD_SHIM_ABI 5 → 6, VarcharShim.rakumod 的 SHIM-ABI 同步抬到 6(低于 6 的缓存库会被丢掉重建)。

  • 踩坑(本版唯一的实质 bug):原 dkd_varchar_unpack 每块都从 total = 0 起算 offset, 写进 offsets 的是块内相对值。多个 chunk 拼进同一缓冲后,第二块的 offset 又从 0 开始, 于是第 2048 行声称自己在字节 0(tc-debug.raku 打印 bad=5,从 i = 2048 起全错)。 修在 C 侧:在 Raku 里逐行补基址实测 503.6 ms/百万行(offbase.raku),不可接受。

  • Raku 侧两趟:pass 1 只累加 sizing(总字节数 + 每行长度),pass 2 按 row-base / byte-base 推进指针写三张表。非 flat 向量(嵌套 / 字典编码)回落 !from-values 逐值解码,语义不变、只是慢。

  • unpack-varchar-into 增 offset-base 参数;运行时遇到 ABI < 6 的旧库走 Raku 兜底循环。

  • 校验:tc-verify-1m.raku 100 万行逐值对比 to-list 0 处不一致;offset 断层扫描 0 处; sum(length) 与 byte-size 精确相等。t/16-text-column.rakutest 45 条断言,覆盖跨 chunk 边界(2048)、inline/pointer 分界(12 字节)、多字节 UTF-8、NULL 与空串区分、拒绝 numeric、 拒绝 decoded-only、无 shim 时的可用性判断。

实测(1M 行 VARCHAR(40),Windows,best-of-3):

出口耗时
column-view($i).text 构建67 ms(0.9.2 复测;0.9.0 首发时记 156 ms)
同列 to-list1 910 ms(整窗解码;逐行 3 910 ms)
byte-size41,896,900

8.7 时序值的 :typed 出口 —— 0.9.1 已完成

动机:at() / column() 为了和 DuckDB 的 ::VARCHAR 逐字一致,把 DATE / TIMESTAMP 渲染成文本 (t/12 里 1.38 / 1.22 µs/值);调用方拿到的是 Str,做日期算术还得自己解析。 at($i, :typed) / to-list(:typed) 反过来:直接把整数载荷造成 Date / UTC DateTime (秒以下保留为 Rat),DATE 用 Date.new(1970,1,1).later(days => $days), TIMESTAMP 用 DateTime.new($secs[, timezone => 0]),floor-div 保证 1970 前的负时间戳落在正确的秒。

实现要点(改之前必读):

  • 原始视图的 typed 段有独立缓存 @!tdec:同一段普通读返回文本、typed 读返回值,两者不能共用一格。

  • 切片 / 批量 / 脱离视图(from-list)拿不到载荷,只能从 DuckDB 自己的文本重建(!typed-from-text); (BC) 后缀、infinity、TIME、TIMESTAMPTZ 一律原样返回文本,宁可给文本也不给错的日期。

  • TIME 与 TIMESTAMPTZ 不做映射:裸时钟 / 带时区的戳没有无损的 Raku 值可落。

  • 覆盖:t/12-temporal.rakutest 53 条(raw / 切片 / 脱离三种视图 × DATE / 三种 TIMESTAMP / 回退形态)。

8.8 execute-many 的 ON CONFLICT 暂存 —— 0.9.1 已完成

历史事实(§8.1 记过):带 ON CONFLICT 的批次只能逐行 prepared,11.3 µs/行量级; 而「灌 TEMP 表再 INSERT ... SELECT」比 appender 慢 850× 的结论只对逐行 statement 成立, 批量回放实测快两个数量级。

现在的路径:语句能解析成 INSERT INTO t [(cols)] VALUES (?...) 且带 ON CONFLICT 尾子句、 批量 ≥ STAGING-MIN-ROWS = 32 时,!insert-plan 把尾子句原样存进 tail,然后

  • CREATE TEMP TABLE __db_duckdb_stage_N AS SELECT <table 全列> FROM <target> LIMIT 0 ——CTAS 复制类型、丢掉约束,正好是 appender 需要的样子;

  • appender 灌暂存表(execute-many 传裸表名即可,别传 :schema<temp>,会报 Table ""."temp".x could not be found);

  • 一条 INSERT INTO <target> (<cols>) SELECT <cols> FROM <stage> <tail> 回放,返回变更行数;

  • LEAVE 里 drop table if exists,失败时上层事务回滚,残留 0 行、0 张表。

判定规则(改之前必读):

  • DO NOTHING 恒可暂存:跳过一行在「逐行」与「一次性」两种语义下结果相同(含批内重复键)。

  • DO UPDATE 只在显式冲突目标 ON CONFLICT (cols) DO UPDATE 时暂存:批量 INSERT ... SELECT 对同一键只应用一次 DO UPDATE,逐行路径是每行一次(末值胜)。0.9.2 起批内重复键不再回落逐行, 而是在回放里先折叠成每键一行(row_number() over (partition by keys order by rowid desc) = 1, rowid 是暂存表的插入顺序,取末行即逐行路径的末值胜);键列含 NULL 的行整组豁免(UNIQUE 允许 NULL 重复)。窗口约占回放时间的三分之一,所以先 group by keys having count(*)>1 探一次, 真重复才建窗口。

  • ON CONSTRAINT、RETURNING、表达式、子查询、窄批次一律走原来的逐行路径(ON CONSTRAINT 是 DuckDB 自己不支持的子句,计划阶段就报错,没有可暂存的东西)。

  • 语义差异(有意接受):批量回放里同一语句的多行对同一目标的 DO UPDATE 只发生一次。

实测(Windows,单进程,含建表开销):20k 行 55.6 s → 0.25 s;100k 行 245.7 s → 0.69 s。 t/05-appender.rakutest 增 14 条断言(DO NOTHING 计数与 DEFAULT 保留、全重复 0 行、临时表清理、 DO UPDATE、批内重复键末值胜、失败回滚无残留、列序置换)。

8.9 TextColumn.slice 零拷贝子视图 —— 0.9.2 已完成

slice($from, $count?) 返回同类的另一个 TextColumn:共享父列的 Buf 与三张表,只多记一个起始行 ($!row0),所以代价与父列大小无关。实测在 1M 行父列上取 1000 次 10 万行切片共 23 ms(≈23 µs/次)。

边界语义(改之前必读):

  • 越界抛异常(slice start 20000 is out of range 0..10000),不静默裁剪;$count 省略表示到末尾。

  • .bytes 仍返回整个父载荷(裁剪就是视图要避免的那次拷贝),.byte-size 返回窗口自身跨度; 这个方法头上的文档写了这一点,不要"顺手"改成裁剪版。

  • 子切片再切片起始行相加,is-slice 恒 True;窗口内 NULL 在 at / to-list 仍读作 Any。

  • t/16-text-column.rakutest 63 条(原 45 条 + slice 的窗口 / 尾窗 / 空窗 / 越界 / 叠加 / NULL)。

8.10 column-arrays 默认并行 + 自适应线程数 —— 0.9.2 已完成

签名改为 Bool :$parallel = !%*ENV<DB_DUCKDB_NO_COLUMN_PARALLEL>,即默认开; worker 数不再恒等于核心数,而是 min(上限, 核心数, $total div PARALLEL-MIN-ROWS), PARALLEL-MIN-ROWS = 20_000,所以 25k 行的小结果仍走串行(线程开销会吃掉收益),百万行才铺开。

实测:1M 行 × 7 列 8.0 s → 4.6 s(1.8×),结果与串行逐值一致。开关三条路径都测了: 默认开、:!parallel 强制关、DB_DUCKDB_NO_COLUMN_PARALLEL=1 整进程关(t/08 31 条)。

8.11 TextColumn 并行构建 —— 做了、测了、删了

本版曾按「byte-size 切块并行」改过 build(!worker-cap / !sizing-spans / !spans 三个辅助方法

  • 两处 promise 扇出),实测没有收益,代码已整体回退为单线程:

载荷1 线程8 线程
1M × 20B51 ms50 ms
1M × 40B67 ms66 ms(4 线程 94 ms,更慢)
1M × 200B196 ms199 ms
2M × 200B355 ms320 ms(唯一一次正收益,1.1×)

原因:两个 pass 几乎全在 shim 内,而 MoarVM 每进程同时只跑一个 NativeCall,worker 只是排队等锁, 额外付出线程调度开销。别再试第二次,除非换到能真正并行跑 native 的运行时。 真要再快只有减少拷贝次数(例如直接读 string_t 布局),不是加线程。

9. 复核命令

# 全测试(Windows)
cd C:\Users\Admin\DB-DuckDB
Get-ChildItem t\*.rakutest | Sort-Object Name | ForEach-Object {
    $o = cmd /c "C:\rakudo\bin\raku.exe -Ilib $($_.FullName) 2>&1"
    "$($_.Name) notok=$(($o | Select-String '^not ok').Count)"
}

# 读写基准
raku -Ilib tools/perf/bench-write.raku <sql|prepared|insert-rows|exec-many|appender> 100000 1000
raku -Ilib tools/perf/bench-varchar-shapes.raku
raku -Ilib tools/perf/bench-parquet.raku 1000000
raku -Ilib tools/perf/bench-temporal.raku 1000000
raku -Ilib tools/perf/probe-dec-enum.raku 1000000

# 零拷贝文本列(构建耗时、offset 连续性、与 to-list 逐值比对)
raku -Ilib t\16-text-column.rakutest

# 惰性视图内存曲线(外部采样子进程 RSS,别在 Raku 里拉 PowerShell)
powershell -File tools/perf/probe-views-rss.ps1 -Rows 200000 -Rounds 200 [-Path chunks]

# 并发安全(首建缓存时多进程同时跑)
1..6 | ForEach-Object { Start-Job { cd C:\Users\Admin\DB-DuckDB; cmd /c "C:\rakudo\bin\raku.exe -Ilib t\13-decimal-enum.rakutest" } } | Wait-Job | Receive-Job

WSL 侧:

export PATH=/home/zby/.rakubrew/versions/moar-2026.09/install/bin:/home/zby/.local/bin:$PATH
cd /mnt/c/Users/Admin/DB-DuckDB
zef install . --install-to=inst#/tmp/dkd-test-$$ --force

10. 发布约定

  • 版本号:SemVer。pre-1.0 阶段 minor 同时承载新功能与行为变更;只有破 DB role 契约时才升 api。

  • 每次发布:META6.json 版本 + CHANGES.md 新段(Fixed / Changed / Added,附实测数字)。

  • 提交风格:<area>: <what> (<version>),例如 Render DECIMAL in C and decode ENUM by dictionary (0.7.0)。

  • 必跑门槛:16 个测试全绿 + Windows 与 WSL 双平台(Linux 首次安装容易漏,0.7.1 就是漏在这里)。

  • 新增 shim 通道清单:C 函数 → SHIM-ABI +1 → Result 探测条件 → digest 三路径比对(C / 纯 Raku / 无 shim)→ 更新 README 的 Design notes 与 Type mapping。

11. 交接检查清单

  • 工作区干净,1f97c25 已推 origin/master

  • Windows:16 个测试 461 个断言 notok=0

  • WSL Ubuntu:zef install Testing [OK] → Installing

  • Parquet 直读写 —— 0.8.0

  • Arrow 无 shim 释放泄漏 —— 0.8.1

  • Arrow 无编译器回落失效 + shim 并发加固 —— 0.8.2

  • shim 缓存清理 + Bridge 的 mtime/并发加固 —— 0.8.3

  • TextColumn.slice 零拷贝视图 + 批内重复键折叠 + column-arrays 默认并行 —— 0.9.2

  • 零拷贝文本列 TextColumn + shim ABI 6 —— 0.9.0

  • 标签 v0.9.0(annotated)已推 origin

  • README 两处过时描述(§8.3)—— 0.7.2

  • stamp-file / abi-stamp 死代码清理 —— 0.7.2

12. 提交历史(近 18 条)

85ad33f feat: slice TextColumn without copying, fold duplicate keys in staged batches, decode columns in parallel by default (0.9.2)
d2c8a7c 0.9.1: typed temporal reads, windowed to-list, staged ON CONFLICT batches
1f97c25 Add DB::DuckDB::TextColumn for zero-copy VARCHAR/BLOB export
e0b1670 fix: fingerprint the bridge build and sweep the shim cache (0.8.3)
46ad0a7 fix: make the Arrow fallback real and harden the shim build (0.8.2)
bab0019 fix: stop the Arrow release path from leaking a call stub per chunk (0.8.1)
30935b7 feat: read and write Parquet through the SQL layer (0.8.0)
595701d docs: record the 0.7.2 cleanup in the handoff checklist (0.7.2)
9e002d9 docs: add the 0.7.1 handoff notes (0.7.2)
f22811a docs: sync the README with 0.7.x and drop the dead shim stamps (0.7.2)
f66db4e Fix Linux install and harden the shim cache (0.7.1)
3be12d3 Render DECIMAL in C and decode ENUM by dictionary (0.7.0)
7c1bdd7 Add a DECIMAL/ENUM probe and record what it says
8c36bb5 Add the temporal benchmark and record the measured numbers
c3aff07 Decode DATE/TIME/TIMESTAMP from their integer payload (0.6.9)
f4b04e3 Record the two rejected candidates; replace the broken view probes
52fadb9 fix: drop the UTF-8 BOM from META6.json
b073e40 Arrow module in META6; exports reachable by full name (0.6.8)
41ae064 docs: bring README in line with 0.6.6/0.6.7
d16ab4f Parallel decode by column weight; double-buffer streaming prefetch

DB::DuckDB v0.9.2

DuckDB driver for the Raku DB interface - NativeCall binding over libduckdb

Authors

  • skyter10086

License

Artistic-2.0

Dependencies

DBAlien::Libduckdb:ver<1.1.0>

Test Dependencies

Provides

  • DB::DuckDB
  • DB::DuckDB::Appender
  • DB::DuckDB::Arrow
  • DB::DuckDB::ArrowShim
  • DB::DuckDB::ArrowShim::Native
  • DB::DuckDB::Bridge
  • DB::DuckDB::Chunk
  • DB::DuckDB::ColumnView
  • DB::DuckDB::Connection
  • DB::DuckDB::Decoder
  • DB::DuckDB::Error
  • DB::DuckDB::Native
  • DB::DuckDB::Parquet
  • DB::DuckDB::Result
  • DB::DuckDB::ShimCache
  • DB::DuckDB::Statement
  • DB::DuckDB::TextColumn
  • DB::DuckDB::VarcharShim
  • DB::DuckDB::VarcharShim::Native

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.