作者:京东零售李小杰
在大数据时代,准确有效的数据是每个致力于长期发展的组织的重要资产之一,而数据测试是不可或缺的一部分。数据测试不仅关注数据处理的代码逻辑,还要考虑大数据执行引擎的影响,因为不同的引擎框架对于相同的数据会产生不同的计算或检索结果。本文将介绍一个年度计费Bug,并讲解在数据测试实践中对大数据执行引擎兼容性差异的探索。
京东- 我的京东- 年度账单每年出具一次,从用户角度总结一年在平台的消费情况。该法案涵盖了购物、权益和服务等方面,帮助用户探索他们难以认知的数据视角。这样,用户就可以从账单中发现自己内心的想法,并主动分享和传播。这次,我们的年度账单以“2022购物印象”为主题,利用不同的数据维度,组成村庄的故事线。用户自始至终都使用虚拟角色。用户浏览故事情节后,可以产生购物印象。
(资料图)
年度账单中的一项报表是用户当年购买的小家电品类。本报告利用年度账单汇总表中的小家电品类设置字段,计算出用户在2022年全年最后购买的两台小家电的品类。本文的Bug分享将围绕该字段展开。
表名
app_my_jd_user_bill_year_sum(用户年度账单汇总表)
字段名称
Small_electrical_appliance_list 小家电类别集合
数据类型
细绳
数据说明
2022年中用户全年最后购买的两款小家电品类清单
号码检索方式
按订单时间倒序取组,输出全年最后购买的两个小家电品类;用| 分隔两个输出类别。
数据源
adm_my_jd_user_bill_month(用户每月账单详细信息)
缺陷描述:APP层用户年度账单汇总模型app_my_jd_user_bill_year_sum中,针对小家电品类采集字段,APP表结果与手动计算结果不一致。
以用户‘水星’、‘乐乐1024’、‘能量男孩’的购买数据为例,上游ADM层以arraystring类型存储用户每月购买的小家电品类,如下所示数字:
?根据小家电品类集字段的定义,APP层应取这三个用户全年购买的最后两个品类,即“水星”11月份购买的VR头显和电炒锅2022年,“乐乐1024”于2022年10月购买了牙刷和空气净化器,“能量男孩”于2022年10月购买了VR头显和电煎锅。因此,经过人工计算,正确的计算结果为APP层应该是:
?APP层面年度账单汇总表小家电征收类别如下,结果有误,不符合预期结果。
在测试和排查过程中,我们首先发现了Hive和Spark引擎之间的语法兼容性差异。
?在APP层脚本中使用小家电品类集合口径构造SQL,手动对上游表执行查询时,发现Hive引擎获取到的集合是有序的,执行结果正确:
?使用Spark引擎执行查询时,集合乱序,执行结果不正确:
缺陷的原因是无序收集导致的访问错误。每个用户在上游ADM中有12个阵列,对应12个月购买的小家电品类集合。需要一个集合函数(collect)将12个月的分组数据进行倒序排序,合并成一个列表,然后取出列表的前两个元素。
HQL提供了两个分组聚合函数:collect_list()和collect_set()。不同之处在于collect_set()将删除重复的列表元素。由于不同月份用户购买的类别集合可能会重复,因此脚本使用collect_set()。
但是collect_set()会导致集合乱序,集合中的元素将不再按月倒序排列。剔除List[0]和List[1]并不是用户全年购买的最后两个小家电品类。
SELECT user_pin,small_electrical_appliance_list, concat_ws('|',small_electrical_appliance_list[0],small_electrical_appliance_list[1]) ASsmall_electrical_applianceFROM( SELECT user_pin,collect_set(concat_ws(',',small_electrical_appliance_list_split)) ASsmall_electrical_appliance_list FROM( SELECT dt ,用户引脚、小型电器列表、concat_ws (',',small_electrical_appliance_list) ASsmall_electrical_appliance FROM adm_my_jd_user_bill_month WHERE dt='2022-01' AND dt='2022-12' ORDER BY dt DESC) tmp 横向视图爆炸(SPLIT(small_electrical_appliance, ',')) tmp ASsmall_electrical_appliance_列表_分割组BY user_log_acct )
? 计算脚本逻辑错误,collect_set() 不应用于聚合分组。
?原生Hive/Spark中,collect_set()函数不能保证集合是有序的,而大数据平台Hive是按顺序计算集合的。因此,这个脚本在Hive引擎下可以达到生成全年购买的最后两个小家电品类的预期目标,但在spark引擎下却无法得到正确的结果。
?Hive执行效率低,研发通常通过Spark引擎执行,最终导致结果不正确。
?collect_set() 和collect_list() 在Presto 中不兼容。
?替代函数:array_agg()(https://prestodb.io/docs/current/functions/aggregate.html?highlight=array_agg#array_agg)
蜂巢/火花
急板
收集列表()
array_agg()
收集集()
array_distinct(array_agg())
?Hive 使用横向VIEWexplode() 执行行到列操作,但Presto 不支持此功能。该单列值将转换为与学生列的一对多行值映射。
Hive/Spark 查询:
横向视图爆炸(SPLIT(small_electrical_appliance, ',')) tmp ASsmall_electrical_appliance_list_split?Presto支持UNNEST扩展数组和映射。文档:(https://prestodb.io/docs/current/migration/from-hive.html)
快速查询:
交叉连接UNNEST(SPLIT(small_electrical_appliance, ',')) ASsmall_electrical_appliance_list_split;
?Hive/Spark支持包括字符串类型到数字类型的多种隐式转换,例如将字符串“07”转换为数字7,然后进行比较操作。
Hive 隐式转换规则:有关详细信息,请参阅链接允许的隐式转换
尽管Presto也有自己的一套隐式类型转换规则,包含在公共OptionalType coerceTypeBase(Type sourceType, String resultTypeBase)方法中,但对数据类型的要求更加严格。对于Hive中一些常见的比较数字和字符串的查询语句,Presto会直接抛出类型不一致错误。
下图展示了Hive和Presto的隐式转换规则。蓝色区域是Presto和Hive都支持的类型转换。绿色区域是Presto不支持但Hive支持的类型转换。红色区域是两者都支持的类型转换。可以看出hive的隐式转换更加广泛,而presto在字符类型的隐式转换方面更加严格。
?隐式转换示例:
--Hive/Spark 隐式转换'07'=6 -- true (CAST('07' AS DOUBLE)=CAST(6 AS DOUBLE))'test' 1 -- NULL'1'=1.0 -- true-- Presto隐式转换'07'=6 -- false (CAST('07' AS Varchar)=CAST(6 AS Varchar))'test' 1 -- true'1'=1.0 -- ERROR:io.prestosql.spi.PrestoException: 意外参数(varchar(1),decimal(2,1)) 函数$operator$equal。预期: $operator$equal(T, T) T:可比较