一、 背景:画像标签系统的"高表"之痛
在我们目前的用户画像(DMP)系统中,为了支持业务方灵活自定义标签,我们采用了**高表(Wide Table)**的存储模型。
1.1 业务场景
-
**核心表结构**:以 `user_id` 为主键,挂载数百个标签列。
-
**痛点**:部分复杂的关联分析数据(如"最近一次搜索词"、"活动参与详情")是动态变化的,无法固化为固定列。
-
**存储妥协**:我们将这些动态数据序列化后存入一个 `TEXT/VARCHAR` 类型的字段(如 `ext_info`)中,格式通常为 类URL 参数形式:`source=baidu&kw=selectdb&cid=1001`。
1.2 遇到的问题
当需要对 `ext_info` 中的 `kw`(搜索词)进行聚合分析时,我们需要在 SQL 中实时解析这个长字符串。
起初我们随意使用了正则或 Map 转换,但在亿级数据量下,查询延迟从毫秒级飙升到分钟级。**究竟哪种解析方式在 SelectDB 中最高效? 本文通过实测告诉你答案。
二、 参赛选手介绍
本次测试的四个"选手"分别是:
-
`**split_part(string, delimiter, index)**`:基础函数,按分隔符切分取第 N 个。
-
`**regexp_extract(string, pattern, index)**`:正则提取,万能但被诟病性能。
-
`**extract_url_parameter(string, key)**`:SelectDB/Doris 专为 URL 设计的内置函数。
-
`**str_to_map(text, pair_delimiter, kv_delimiter)**`:将字符串转为 Map 对象,再按键取值。
三、 测试环境与数据
-
**环境**:SelectDB Cloud (3 BE节点, 16C 64G)
-
**数据量**:300万行(为了放大差异,特意构造了较长的 `ext_info` 字符串,包含 20 个键值对)。
-
**目标**:提取中间的 `kw` 参数。
四、 性能对比与 Profile 深度分析
4.1 第一组:正则 vs 专用函数
**SQL A (正则):**
```sql
SELECT regexp_extract(ext_info, 'kw=(\^\&*)', 1) FROM user_tags LIMIT 10000;
```
**SQL B (专用):**
```sql
SELECT extract_url_parameter(concat('?', ext_info), 'kw') FROM user_tags LIMIT 10000;
```
**执行结果:**
-
**SQL A 耗时**:1.2s
-
**SQL B 耗时**:0.3s
**Profile 分析:**
查看 `Fragment Instance Profile`,我们发现:
-
`**regexp_extract**`:在 `OLAP_SCAN` 阶段,`ReadRowsTime` 占比极高。正则引擎在处理 `(\^\&*)` 时,涉及大量的状态机跳转和回溯,CPU 指令周期长。
-
`**extract_url_parameter**`:底层是 C++ 针对 `&` 和 `=` 做的指针硬解析,逻辑极其简单,几乎没有额外的 CPU 开销。
4.2 第二组:str_to_map 的"陷阱"
很多开发同学觉得 `str_to_map` 语义最清晰,写法最优雅:
**SQL C (Map):**
```sql
SELECT str_to_map(ext_info, '&', '=')'kw' FROM user_tags LIMIT 10000;
```
**执行结果:**
-
**SQL C 耗时**:2.5s (最慢!)
-
**内存峰值**:是其他方案的 3 倍。
**深度解析:**
为什么它最慢?看执行计划中的 `Deserialize` 阶段。
`str_to_map` 必须**遍历整个字符串**,将 `source`, `kw`, `cid` 等所有 20 个键值对全部解析出来,并在内存中构建一个哈希表对象。而我们只需要其中的 `kw`。
**结论**:如果你只需要取 1-2 个字段,用 `str_to_map` 属于"杀鸡用牛刀",且这把刀特别重。
4.3 第三组:split_part 的"奇袭"
如果参数位置固定,比如 `kw` 永远在第 2 位:
**SQL D (Split):**
```sql
SELECT split_part(split_part(ext_info, '&', 2), '=', 2) FROM user_tags LIMIT 10000;
```
**执行结果:**
- **SQL D 耗时**:0.25s (最快)
**分析:**
`split_part` 仅仅是做内存偏移和拷贝,不涉及任何逻辑判断。但它太脆弱了,一旦 `source` 参数缺失,`kw` 变成了第 1 位,数据就错了。
五、 综合性能对比表
| 函数名称 | 核心机制 | CPU 耗时 (30万行) | 内存开销 | 推荐指数 | 评价 |
|---|---|---|---|---|---|
| split_part | 指针偏移 | ⭐ (最低) | ⭐ (最低) | ⭐⭐⭐ | 简单场景无敌,但灵活性差。 |
| extract_url_parameter | 专用解析 | ⭐⭐ (很低) | ⭐⭐ (低) | ⭐⭐⭐⭐⭐ | URL 场景的最优解,性能与语义的完美平衡。 |
| regexp_extract | 状态机匹配 | ⭐⭐⭐⭐ (高) | ⭐⭐⭐ (中) | ⭐⭐ | 除非格式太乱,否则不建议用于简单提取。 |
| str_to_map | 全量对象构建 | ⭐⭐⭐⭐⭐ (极高) | ⭐⭐⭐⭐⭐ (极高) | ⭐ | 提取单字段时严禁使用,资源杀手。 |
六、 避坑指南与最佳实践
结合画像标签系统的实战经验,给出以下建议:
- **首选 **`**extract_url_parameter**`:
这是 SelectDB/Doris 提供的"特种兵"函数。虽然它通常需要配合 `concat('?', col)` 使用(因为标准 URL 带问号),但其内置的解析逻辑远快于正则。
- **慎用 **`**str_to_map**`:
除非你需要一次性提取字符串中的 5 个以上字段,否则不要用它。在海量数据扫描场景下,它构建 Map 对象的开销会直接拖垮 BE 节点内存。
- **正则不是万能药**:
在处理简单分隔符(`&`, `=`)时,正则的性能损耗是巨大的。Profile 中看到的 `VectorizedFunctionCallExpr` 耗时过高往往就是正则惹的祸。
- **关于 NULL 处理**:
`extract_url_parameter` 如果找不到 Key 会返回 NULL,这与业务逻辑契合;而 `split_part` 如果越界也可能返回空或错误数据,需要在 ETL 层做严格校验。
七、 总结
在 SelectDB 中处理长字符串标签时, **"专用函数 > 简单切分 > 正则 > 对象转换"** 。
对于画像系统中的 `ext_info` 解析,请毫不犹豫地选择 `extract_url_parameter`,它能让你的即席查询(Ad-hoc)速度提升一个数量级。
*希望这篇基于实战的分析能帮你避开性能深坑!如果觉得有用,欢迎点赞收藏。*