
见字如面,我是一臻
❝我是 Apache Doris,一个AI+实时分析一体的数据库。 老实说,刚被货拉拉
相中那会儿,我也挺忐忑——毕竟他们之前试过 Impala + Kudu、Elasticsearch,都没搞定。 亿级用户、 3000+ 标签体系、还有那些让人头大的嵌套圈人规则……这活儿确实不轻松啊!但咱有 MPP 架构、向量化引擎,还有那个专为用户画像场景量身定制的 BITMAP 神器...
第一次见到货拉拉团队的技术架构图时,我就知道这活儿不简单。
他们当时用的是 Impala+KUDU 组合,说实话这套架构在好几年前也算是主流方案。但问题是,业务增长的速度远超当初的预期。
"这个人群又超时了,运营那边等着急呢!"他们盯着屏幕上那条慢悠悠的进度条,额头上都冒汗了。这已经是本周第N次因为圈人任务超时被催了。
我在旁边的测试环境里默默观察着:一个看似简单的人群圈选,在 Impala+KUDU 上跑了整整十几分钟还没出结果。更要命的是,每天凌晨的数据导入任务,4 亿行数据要折腾 90 多分钟,运维同学估计得盯到凌晨三点半才敢去睡觉。
"要不换个引擎试试?"有人提议。于是他们尝试了 Elasticsearch。
说实话,ES 在某些场景下确实有改善,但它的强项毕竟是全文检索,面对用户画像这种多维分析场景,还是有些力不从心。复杂查询的 DSL 语法让开发同学抓狂,涉及人群嵌套依赖的业务场景更是束手无策。
"这也不行,那也不行,到底该选谁?"技术选型会上,团队陷入了沉思。

就在这时,有人提到了我:"要不试试 Doris?听说它在 OLAP 分析方面表现不错。"
坦白说,市面上能做 OLAP 分析的开源产品不少,货拉拉为什么最终选择了我?我自己心里也琢磨过这个问题。
后来听他们在复盘会上似乎在说:"选数据库就像组队打副本,得看它能不能扛伤害、输出够不够猛、关键时刻会不会掉链子,还得看出了问题能不能找到人帮忙。"
这几条标准,我确实都符合。

首先是性能。
我采用的 MPP(大规模并行处理)架构,配合向量化执行引擎和 CBO 优化器,在复杂查询场景下的表现确实够硬核。
用户画像这种需要频繁做多维分析、聚合计算的场景,正是我的强项。
其次是灵活性。
我支持多种数据模型:Aggregate、Unique、Duplicate,还有专门为画像场景优化的 BITMAP 类型。
这意味着不同特性的标签数据,都能找到最适合自己的存储方式,而不是被迫塞进一个僵硬的框架里。
再就是生态。
我背后有个非常活跃的开源社区,文档详实,问题响应快。
货拉拉团队在迁移过程中遇到的几个技术难点,都是在社区群里得到了快速解答,有些 PMC 成员甚至直接下场帮忙分析问题。
但我最大的杀手锏,其实是 BITMAP。
如果说我身上有什么功能是为用户画像量身定制的,那一定是 BITMAP。
用户画像的核心操作是什么?圈人。
圈出"北京的用户",圈出"男性用户",再把两个圈子取交集,得到"北京的男性用户"。这种交集、并集、差集的运算,如果用传统的 SQL JOIN 来做,性能会非常糟糕。
但如果用 BITMAP 呢?一切都变得优雅起来。
我把每个用户群体用一个位图来表示,比如用户 ID 为 1、3、5 的三个人,在位图里就是"101010..."这样的 0 和 1 序列。两个位图做交集,就是把对应位置的 0 和 1 做"与"运算;做并集,就是做"或"运算。这些位运算的速度快到你无法想象,而且 BITMAP 还使用了 RoaringBitmap 压缩算法,存储空间极其节省。
"一个包含百万用户的人群,用 BITMAP 存储可能只需要几十 KB。"大家第一次看到这个数据时,眼睛都亮了。
更妙的是,BITMAP 的计算结果还是 BITMAP,这意味着可以无限嵌套。无论运营同学提出多么复杂的规则——"(A 且 B)或(C 且(D 或 E))且排除 F"——我都能通过 BITMAP 的递归计算优雅地搞定。

"这就是我要的!"他们在测试环境里跑完第一个 BITMAP 查询后,兴奋地喊了出来。原本要十几分钟的圈人任务,现在只需要秒级别!
光有好的工具还不够,关键是怎么用。
货拉拉团队在我身上设计的存储方案,可以说是把我的能力发挥到了极致。
他们没有简单粗暴地把所有标签塞进一张大宽表,也没有全部用高表存储,而是根据标签的特性,设计了三种存储模型协同工作。
第一种是标签宽表
CREATE TABLE wide_table (
user_id varchar(1000) NULLCOMMENT"",
age bigint(20) REPLACE_IF_NOT_NULL NULLCOMMENT"",
height bigint(20) REPLACE_IF_NOT_NULL NULLCOMMENT"",
......) ENGINE=OLAP AGGREGATEKEY( user_id ) COMMENT"OLAP"
DISTRIBUTEDBYHASH( user_id ) BUCKETS 40
PROPERTIES ( ... )
那些更新频率低、相对稳定的核心标签,比如用户的年龄、注册地、账户...都存在宽表里。
虽然会存在上百列,但我的列式存储引擎可以做到用哪列读哪列,查询时只加载需要的列,性能一点都不受影响。
而且这些标签之间经常需要做复杂的多维分析,宽表的结构正好适合这种场景。
第二种是标签高表
CREATE TABLE high_table (
tag varchar(45) NULLCOMMENT"标签名",
tag_value varchar(45) NULLCOMMENT"标签值",
time datetime NOTNULLCOMMENT"数据灌入时间",
user_ids bitmap BITMAP_UNION NULLCOMMENT"用户集"
) ENGINE=OLAP AGGREGATEKEY( tag , tag_value , time ) COMMENT"OLAP"
DISTRIBUTEDBYHASH( tag ) BUCKETS 128
PROPERTIES ( ... )
那些高频更新、分布稀疏的动态标签,都存在高表里。
每行记录"用户ID、标签名、标签值",新增标签只需要插入数据,不用改表结构。
我的 Aggregate 模型配合 BITMAP_UNION 聚合函数,可以把这些稀疏数据高效地聚合成 BITMAP,查询性能照样飞快。
第三种是人群位图表
CREATE TABLE routine_segmentation_bitmap (
time datetime NOTNULLCOMMENT"数据灌入时间",
seg_name varchar(45) NULLCOMMENT"标签值",
user_ids bitmap BITMAP_UNION NULLCOMMENT"人群ID集合"
) ENGINE=OLAP AGGREGATEKEY( time , seg_name )
COMMENT"OLAP"PARTITIONBYRANGE(`time`) (...)DISTRIBUTEDBYHASH( seg_name )
BUCKETS 128
PROPERTIES (..., "dynamic_partition.enable" = "true", ...);
这是整个方案的点睛之笔。
每个圈选出来的人群结果,都以 BITMAP 的形式存在这张表里。这样一来,当运营提出"基于人群 A 和人群 B 的交集,再排除人群 C"这种依赖关系时,我只需要读取三个 BITMAP,做两次运算,一秒钟就能得到最终结果。不需要重新扫描原始数据,不需要新建表,存储空间也省了。
"之前我们最怕的就是人群嵌套依赖,现在有了位图表,这种需求反而成了最简单的。"大家在团队分享会上说,"Doris 的 BITMAP 真的是为用户画像而生的。"

三张表各司其职,但又能通过我的 UNION ALL 和 BITMAP 函数无缝协作。
当一个复杂的圈人规则下发时,我会自动识别哪些条件走宽表、哪些走高表、哪些直接读取人群位图,然后在最外层做一次 BITMAP 聚合,把结果吐出来。
整个过程行云流水,用户在前端拖拖拽拽,我在后端自动选择最优路径执行。
架构设计好了,但还有最后一公里要走——SQL 优化。
最初,运营同学在可视化界面上拖出一个复杂规则后,前端会生成 DSL 描述,然后直接翻译成 SQL 丢给我。这种翻译逻辑比较憨厚,用户拖什么就翻译什么,导致生成的 SQL 经常有大量冗余操作。
比如,用户想圈"东莞的男性用户",前端会生成两个条件:"city='东莞'" 和 "sex='男'"。然后翻译器会老老实实地生成两个子查询,一个查东莞用户得到 BITMAP A,一个查男性用户得到 BITMAP B,最后把 A 和 B 做交集。
"等等!"有人看着执行计划皱起了眉头,"这两个条件明明都在同一张宽表里,为什么要查两次再合并?直接一个 WHERE 条件 city='东莞' AND sex='男' 不就完了?"
这就是优化的空间。

货拉拉团队在 SQL 生成之前,加了一层智能优化器,对 DSL 进行预处理。
第一招叫条件合并,也叫"染色"。
优化器会分析 DSL 树,把能合并的同类条件聚到一起。原本需要查三次宽表、生成三个 BITMAP 再合并的操作,优化后变成一次查询、一个 BITMAP 生成,直接省掉 60% 的冗余计算。
第二招叫结构扁平化,也叫"剪枝"。
复杂的嵌套逻辑会被拉平,多余的 AND/OR 节点会被移除。原本可能有五六层的子查询嵌套,优化后变成两三层,执行计划的复杂度直线下降。
这些优化对我来说也是实实在在的减负。扫描次数少了,中间结果少了,内存占用自然就降下来了。在业务高峰期,原本我的内存占用会飙到 60%,优化后稳定在 20% 以内。JVM 的堆内存使用也从濒临告警的边缘,降到了安全舒适区。
"现在高峰期跑人群任务,我都敢去接杯水再回来看结果了。"他们笑着说,"以前可不敢,得死死盯着,生怕内存爆了。"
人群圈选的结果最终要推送给下游业务系统使用,这就涉及到位图数据的读取。别看只是个读操作,里面的学问可不少。

最简单的方式是用我自带的 bitmap_to_string 函数,把位图转成逗号分隔的字符串。这种方式适合小数据量的测试场景,但一旦人群规模变大后,字符串会膨胀到不可接受的程度,网络传输和解析都成了瓶颈。
第二种方式是用 explode 函数把位图"展开"成用户列表,配合 lateral view 做流式处理。这招在做多表关联分析时很好用,但代价是我要先在服务端把整个位图展开并缓存,高峰期会给我带来不小的压力。
货拉拉团队最终采用的是第三种方案:直接读取位图的二进制数据。
他们在客户端设置 return_object_data_as_binary=true,让我直接把位图的原始二进制数据传给他们,然后在画像服务端用我的源码协议进行反序列化。
这种方式传输的数据量最小,内存占用最低,在高峰期每分钟可以轻松处理几十上百个人群任务。
"这就好比搬一箱书,是把每本书拆开一本本搬,还是直接扛着箱子走?"我觉得这个比喻较为形象,"位图的二进制数据就是那个箱子,不用打开,直接传输,到了地方再拆。"
这种细节上的优化,体现了货拉拉团队对我能力的深度理解和充分利用。
不是简单地"用我",而是"用好我"。
除了查询,数据导入也是个大工程。
每天凌晨,货拉拉的数据团队要把 Hive 里更新的 4 亿行、200 多列的用户数据灌到我这里来。这可不是个小活儿,在 Impala+KUDU 时代,这个过程要耗费 90 分钟以上,而且经常因为超时失败,运维同学得盯到凌晨三点半才敢睡觉。
换到我这儿之后,情况立刻改善了。

我支持 Broker Load 方式,可以直接从 HDFS 读取数据并行导入。原本 90 分钟的活儿,现在 30 分钟内就搞定了,效率提升了整整 3 倍。
"第一次跑完数据导入任务,看着监控大盘上显示的 30 分钟,我还以为是哪里出错了。"他们回忆道,"反复检查了好几遍,确认数据完整无误后,我才敢相信这是真的。"
对于高表和人群位图表,导入就更简单了。实时和近实时的标签数据,通过 Flink 实时写入我这里,延迟控制在秒级或分钟级。稀疏标签使用高表存储,5 分钟就能完成一次全量更新。
数据导入的提速,不仅仅是省了时间,更重要的是提升了整个数据链路的时效性。运营同学可以更早拿到最新数据,营销活动的响应速度也更快了。
如今,我已经在货拉拉稳定运行了两年多,支撑着 300 多个业务场景,管理着 3000 多个标签和 5 万多个人群。每天处理的查询请求数以万计,但再也没有出现过那种让人焦虑的长时间等待。
从数据上看,我交出的成绩单还算不错:
1. 查询效率提升 30 倍,原本几十分钟的任务现在秒级响应
2. 数据导入效率提升 3 倍,从 90 分钟缩短到 30 分钟
3. 内存开销降低 30%-50%,高峰期从 60% 降到 20%
4. 系统稳定性质的飞跃,两年多没出过重大故障
回顾这段从"被选中"到"成为主力"的旅程,我最大的感悟是:技术选型真的就像找对象,不是谁最完美,而是谁最合适。
Impala+KUDU 不是不好,它只是不适合千万级用户、高并发、复杂查询的场景。Elasticsearch 也不是不行,它只是强项不在多维分析。而我,恰好在用户画像这个场景下,有 MPP 架构、向量化引擎、BITMAP 类型、灵活的数据模型这些恰到好处的能力。
更重要的是,货拉拉团队没有简单地用我,而是深入理解我的特性,精心设计存储方案,持续优化查询逻辑,把我的能力发挥到了极致。这种双向奔赴,才是技术与业务完美结合的关键!
我是 Doris,一个找到了自己位置的数据库。
货拉拉和我的故事,还在继续...
你呢?