先别反复搬整块招牌
想象一张列车停靠记录:每到一站,表里都重新写一遍“Amsterdam Centraal”。现在要统计每个车站有多少次停靠,最直观的办法是直接按站名分组。但这相当于清点货物时,每次都搬起整块招牌核对,而不是先看一个短编号。
DuckDB Team 给出的优化很朴素:先给每个站名分配一个小整数,聚合时只处理整数,最后再把编号连接回站名。DuckDB 是一种面向分析查询的嵌入式列式数据库,分析师可以直接在本机读取 Parquet、CSV 等文件,不必先部署数据库服务器。
这值得关注,不是因为它发明了新算法,而是因为它把数据仓库里经典的维度建模技巧,落实成了一条容易复现的查询优化路径。需要先说明:本文数字与效果判断均来自 DuckDB Team 的单篇官方博客;文章没有提供加速倍数、耗时或硬件环境,因此这里只解释机制,不把它写成普遍成立的性能承诺。
慢的不是计数,而是反复认名字
官方演示使用公开的 train_services 数据集。它有 380,959 行,却只有 537 个不同的 station_name。其中 Amsterdam Centraal 占 18 bytes,在表中出现了 7,591 次。这里的 18 bytes 是该 ASCII 字符串在演示中的描述,不等同于通常所说的字符数,也不能代表所有编码下的存储开销。
GROUP BY 聚合,就是把相同类别的记录归到一起,再计算数量、总和或平均值。DuckDB 用哈希聚合执行这项工作:它先计算分组键的哈希值,用它寻找哈希表中的候选位置,再比较实际的键;每个分组在表里保留一个条目。
如果键是整数,哈希和比较只需处理固定宽度的数据。如果键是字符串,工作量会随字符串长度增长。直接按站名分组时,DuckDB 要为每一行读取并哈希完整字符串。遇到候选位置后,还要比较分组键;第一次遇到某个站名时,还要把它复制进哈希表。
DuckDB 中一个字符串值本身是 16-byte 结构。最长 12 bytes 的短字符串可以直接放在结构里;更长的字符串则保存 4-byte 前缀和一个指向实际字符的指针。确认两个长字符串相同时,仅看前缀不够,还要沿指针读取并比较完整内容。字符串也会让哈希表更宽,影响内存使用;在超出内存的聚合中,还可能更早触及限制并把更多数据写到磁盘。
DuckDB 会对磁盘上的字符串列使用字典编码——用小编号代替重复文字的存储优化。但官方文章指出,列进入聚合后,分组键仍是完整字符串。磁盘上压缩过,不等于计算时已经变成整数。
给站名发号码牌
解决办法是建立维表。维表把站名、产品名等描述性文字单独保存一份;记录业务事件的事实表只存一个指向它的整数键。查询先按整数统计,最后通过连接,也就是按共同键把两张表重新对应起来,取回可读的名称。
第一步是数清不同值有多少。537 个站名决定了键至少要多宽。官方示例使用从 1 开始递增的 row_number(),因此选择不为负数预留范围的无符号整数:UTINYINT 占 1 byte,最多容纳 255 个值;USMALLINT 占 2 bytes,最多容纳 65,535 个值;UINTEGER 占 4 bytes,可覆盖约 43 亿个值。
537 超过 255,所以示例选择 USMALLINT。接着为每个不同站名分配整数 ID,把 ID 写回事实表。此后统计各站停靠次数时,数据库处理的是 2-byte 整数,而不是每行重复出现的站名;输出结果时,再连接维表恢复名称。
如果整数键的取值范围足够小,DuckDB 还有机会使用 perfect hash aggregate——不再计算普通哈希,而是直接把键值当作数组位置。是否采用它由优化器依据统计信息判断,并受 perfect_ht_threshold 设置控制。
老技巧为什么仍值得写进指南
据 DuckDB 介绍,这篇文章起于一次“查错方向”的用户反馈:有人报告高基数分组占用大量内存。高基数指一列包含很多不同值。团队调查后发现,那个案例的问题并不在字符串上;但工程师 Richard Wesley 想起,自己曾把重复字符串拆进维表,最后再连接回来,对字符串密集型聚合带来明显改善。这个经验随后被加入 DuckDB Performance Guide 的 schema 章节。
它本质上是星型模式:中心事实表保存大量事件和紧凑的键,外围维表保存名称等描述信息。它也解释了这项技巧的真正价值——不是把一次查询写得更花,而是把重复文字从频繁计算的路径中搬走,并让同一套映射可以跨查询复用。
局限与未知
- 官方摘录没有给出加速倍数、执行时间、内存占用、硬件环境、DuckDB 版本或对照实验,无法判断实际收益的量级。
- 建维表、生成编码和执行连接本身都有成本。收益取决于行数、字符串长度、不同值数量、映射复用程度和查询形态,不能概括成“字符串总是更贵”或“先编码一定更快”。
- 键宽要为未来增长留余量。若不同值数量超过整数类型的范围,就必须改用更宽的键;本次供稿在建表步骤的
ORDER BY细节处截断,无法进一步说明官方的完整建表语句。