最近接手了一个数据仓库迁移项目,需要将原有基于ClickHouse的路口安全指标分析系统迁移到Apache Doris平台。这个过程中最头疼的就是处理那些嵌套数组字段——在ClickHouse里我们用arrayJoin玩得飞起,但到了Doris发现这个函数根本不存在。就像突然被没收了趁手工具的老木匠,我盯着那些压缩在单条记录里的多维指标数据直发愁。
路口安全指标表的典型结构是这样的:每条记录包含路口编号、时间戳等基础字段,还有approach_index这样的数组字段,里面打包存储了各个进口方向(东、南、西、北)的步行违法率、交通冲突数等十几项指标。这种"宽表"设计虽然节省存储空间,但在实际分析时需要频繁展开数组,特别是在需要按单个进口方向进行统计分析时。
ClickHouse的arrayJoin用起来确实方便,一个简单的SELECT arrayJoin(approach_index) FROM table就能把嵌套数据展开。但Doris采用了不同的技术路线,它提供的解决方案是Lateral View配合Explode表函数的组合拳。刚开始接触这套方案时,我花了三天时间才彻底理解其运作机制,现在回想起来,那些踩过的坑都是宝贵的经验。
Explode这个表函数名字起得很形象——就像引爆一个装满数据的炸弹,把压缩的数组炸开成多行数据。在Doris中执行EXPLODE([1,2,3])会返回三行记录,每行包含一个数组元素。但单独使用Explode有个限制:它只能处理明确的数组字面量或数组列,无法直接与其他字段组合查询。
这里有个实际踩过的坑:最初我尝试写SELECT time_stamp, EXPLODE(approach_index) FROM table,结果Doris直接报错。原来Explode作为表函数(table function),需要特殊的语法支持才能与普通字段一起查询——这就是Lateral View的用武之地。
Lateral View相当于在查询过程中创建了一个临时虚拟表,它能把Explode展开的数据与原表其他字段智能地关联起来。这个"横向视图"会为原表的每一行生成若干新行(取决于数组元素个数),就像Excel里的"拆分单元格"功能,但更加灵活强大。
具体到我们的路口指标表,LATERAL VIEW EXPLODE(approach_index) tbl1 AS sub这段代码实现了:
这种机制完美解决了宽表转高表的需求。我实测过一个包含10万条记录的表,每条记录的approach_index数组平均有4个元素,转换后得到约40万行的高表数据,查询性能比在应用层做处理快了近20倍。
先看最基础的数组展开场景。假设原始表结构如下:
sql复制CREATE TABLE dwd_signal_securityindex_ri (
time_stamp DATETIME,
intersection_number VARCHAR(20),
safety_factor DECIMAL(5,2),
approach_index ARRAY<STRING> -- 格式如 ["东-0.85-0.12-3", "南-0.78-0.15-2"]
);
迁移前的ClickHouse查询可能是这样的:
sql复制SELECT
time_stamp,
intersection_number,
arrayJoin(approach_index) AS approach_data
FROM dwd_signal_securityindex_ri
对应的Doris迁移方案:
sql复制SELECT
time_stamp,
intersection_number,
sub AS approach_data
FROM dwd_signal_securityindex_ri
LATERAL VIEW EXPLODE(approach_index) tbl1 AS sub
这里有个性能优化点:Doris的向量化引擎对这类操作有特别优化,建议先执行SET enable_vectorized_engine = true开启向量化支持。在我的测试中,这能使查询速度提升30%左右。
实际场景往往更复杂。我们的approach_index数组元素是字符串,用连字符分隔不同指标值。比如"东-0.85-0.12-3"表示:
这就需要配合SPLIT_BY_STRING函数进行二次拆分:
sql复制SELECT
time_stamp,
intersection_number,
SPLIT_BY_STRING(sub, '-') AS metrics_array
FROM dwd_signal_securityindex_ri
LATERAL VIEW EXPLODE(approach_index) tbl1 AS sub
为了得到规整的明细数据,还需要用ELEMENT_AT函数从数组中提取具体元素。完整方案如下:
sql复制SELECT
time_stamp,
intersection_number,
ELEMENT_AT(metrics_array, 1) AS approach_direction,
CAST(ELEMENT_AT(metrics_array, 2) AS DECIMAL(5,2)) AS pedestrian_time_guarantee_rate,
CAST(ELEMENT_AT(metrics_array, 3) AS DECIMAL(5,2)) AS pedestrian_illegal_rate,
CAST(ELEMENT_AT(metrics_array, 4) AS INT) AS traffic_conflict
FROM (
SELECT
time_stamp,
intersection_number,
SPLIT_BY_STRING(sub, '-') AS metrics_array
FROM dwd_signal_securityindex_ri
LATERAL VIEW EXPLODE(approach_index) tbl1 AS sub
) t
这里特别注意类型转换操作。Doris的SPLIT_BY_STRING返回的是字符串数组,必须显式转换为数值类型才能进行后续计算分析。
对于频繁查询的指标,建议创建物化视图。比如路口进口级别的安全指标视图:
sql复制CREATE MATERIALIZED VIEW dwd_signal_securityindex_ri_mv
DISTRIBUTED BY HASH(intersection_number)
REFRESH ASYNC
AS
SELECT
time_stamp,
intersection_number,
ELEMENT_AT(metrics_array, 1) AS approach_direction,
CAST(ELEMENT_AT(metrics_array, 2) AS DECIMAL(5,2)) AS pedestrian_time_guarantee_rate,
CAST(ELEMENT_AT(metrics_array, 3) AS DECIMAL(5,2)) AS pedestrian_illegal_rate,
CAST(ELEMENT_AT(metrics_array, 4) AS INT) AS traffic_conflict
FROM (
SELECT
time_stamp,
intersection_number,
SPLIT_BY_STRING(sub, '-') AS metrics_array
FROM dwd_signal_securityindex_ri
LATERAL VIEW EXPLODE(approach_index) tbl1 AS sub
) t
物化视图能极大提升查询效率,特别是在需要聚合统计的场景下。测试显示,对转换后的高表进行GROUP BY approach_direction操作,使用物化视图后查询耗时从原来的12秒降到了0.8秒。
实际数据中常会遇到空数组或NULL值。Explode遇到空数组时会直接跳过该行,这可能造成数据丢失。解决方案是改用EXPLODE_OUTER:
sql复制SELECT
time_stamp,
intersection_number,
sub AS approach_data
FROM dwd_signal_securityindex_ri
LATERAL VIEW EXPLODE_OUTER(approach_index) tbl1 AS sub
EXPLODE_OUTER会为NULL或空数组生成一行NULL值记录,确保原表记录数完整。不过要注意,这可能导致下游处理时需要额外的NULL检查。
有些表设计会有多个数组字段需要同时展开。比如除了approach_index,还有phase_index存储相位指标。这时可以使用多个LATERAL VIEW:
sql复制SELECT
t.time_stamp,
t.intersection_number,
a.sub AS approach_data,
p.sub AS phase_data
FROM dwd_signal_securityindex_ri t
LATERAL VIEW EXPLODE(approach_index) a AS sub
LATERAL VIEW EXPLODE(phase_index) p AS sub
这种写法会产生笛卡尔积,即每个approach元素会与每个phase元素组合。如果这不是你想要的效果,就需要考虑分步处理或修改数据结构。