作者:京东批发 李晓洁
咱们经常遗记,蠢才也取决于其所能把握的数据,即便阿基米德也无奈设计出爱迪生的创造。——Ernest Dimnet
在大数据时代,精准而无效的数据对于每个致力于长期倒退的组织来说都是重要资产之一,而数据测试更是不可或缺的一部分。数据测试不仅关注数据加工的代码逻辑,还要思考大数据执行引擎带来的影响,因为各种引擎框架将对同一份数据产生不同的计算或检索后果。本文将从一个年度账单 bug 引入,解说在数据测试实际中对大数据执行引擎兼容性差别的摸索。
一、需要内容
京东 - 我的京东 - 年度账单是一年一次,以 用户视角 对在平台一年的生产状况进行总结。账单从购物,权利,服务等方面切入,帮忙用户开掘在自我难以认知的数据角度,通过这种形式让用户从账单中挖掘感动心田的立意,并被动进行分享和流传。本次,我京年度账单以“2022 购物印象”为主题,通过不同的数据维度组成村落故事线,用户以虚构人物形象贯通始终,用户浏览完故事线后,可生成购物印象。
年度账单其中一个报表为用户年度购买的小家电品类。该报表应用年度账单汇总表中的小家电品类汇合字段,计算了 2022 年度某用户全年最初购买的两款小家电所在的品类。本文 bug 分享将围绕这个字段开展。
二、缺点形容
缺点形容:在 APP 层用户年度账单汇总模型 app\_my\_jd\_user\_bill\_year\_sum 中,对于小家电品类汇合字段,APP 表后果与手动计算结果不统一。
以用户 ’Mercury’、’ 乐乐 1024’、’ 生机少年 ’ 的购买数据为例,上游 ADM 层以 array<string>
类型存储用户每月购买的小家电相干品类,如下图所示:
• 依据小家电品类汇合字段定义,APP 层应取这三个用户全年最初购买的 2 个品类,即 ’Mercury’ 在 2022 年 11 月购买的 VR 头戴显示器、电炒锅,’ 乐乐 1024’ 在 2022 年 10 月购买的冲牙器、空气净化器,’ 生机少年 ’ 在 2022 年 10 月购买的 VR 头戴显示器、电炒锅。因而,经手动计算,APP 层正确计算结果应为:
• 而 APP 层年度账单汇总表中的小家电汇合品类如下,后果谬误,不合乎预期后果。
三、缺点排查过程
1. 执行引擎兼容差别
测试排查中,首先发现了 Hive 和 Spark 引擎之间的语法兼容差别。
• 当应用 APP 层脚本中小家电品类汇合口径构建 SQL,手动对上游表执行查问时发现,Hive 引擎失去的汇合有序,执行后果正确:
• 应用 Spark 引擎执行查问时,汇合乱序,执行后果谬误:
2. 脚本梳理
缺点起因为汇合乱序导致的取数谬误。每个用户在上游 ADM 存在 12 个数组对应 12 个月购买小家电品类的汇合,须要汇合函数(collect)将 12 个月分组数据倒序排序,会合成 1 个列表,而后取列表前两个元素。
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]) AS small_electrical_appliance
FROM(
SELECT
user_pin,
collect_set(concat_ws(',', small_electrical_appliance_list_split)) AS small_electrical_appliance_list
FROM(
SELECT
dt,
user_pin,
small_electrical_appliance_list,
concat_ws(',', small_electrical_appliance_list) AS small_electrical_appliance
FROM adm_my_jd_user_bill_month
WHERE
dt >= '2022-01'
AND dt <= '2022-12'
ORDER BY dt DESC) tmp
lateral VIEW explode(SPLIT(small_electrical_appliance, ',')) tmp AS small_electrical_appliance_list_split
GROUP BY user_log_acct )
3. 论断
• 计算脚本逻辑谬误,不应应用 collect_set()
聚合分组。
• 在原生 Hive/Spark 中,collect_set()
函数均无奈保障汇合有序,而大数据平台 Hive 对汇合计算有序。因而,该脚本在 Hive 引擎下能够达到生成全年最初购买两个小家电品类的预期指标,但 spark 引擎则无奈失去正确后果。
• Hive 执行效率较低,研发通常通过 Spark 引擎执行,最终导致后果谬误。
四、大数据计算引擎兼容差别
1. collect\_list()/collect\_set() 在 hive/spark 和 presto 之间的区别
• collect_set()
与 collect_list()
在 Presto 中无奈兼容。
• 代替函数:array_agg()
(https://prestodb.io/docs/current/functions/aggregate.html?highlight=array\_agg#array\_agg)
Hive/Spark | Presto |
---|---|
collect_list() | array_agg() |
collect_set() | array\_distinct(array\_agg()) |
2. 行转列函数在 hive 和 presto 之间的区别
• Hive 应用 lateral VIEW explode()
执行行转列的操作,而 Presto 不反对该函数。这种单列的值转换成和 student 列一对多的行的值映射.
◦ Hive/Spark query:
lateral VIEW explode(SPLIT(small_electrical_appliance, ',')) tmp AS small_electrical_appliance_list_split
• Presto 反对 UNNEST
来扩大 array 和 map。文档:(https://prestodb.io/docs/current/migration/from-hive.html)
◦ Presto query:
CROSS JOIN UNNEST(SPLIT(small_electrical_appliance, ',')) AS small_electrical_appliance_list_split;
3. 隐式转换在引擎之间的区别
• Hive/Spark 反对包含字符串类型到数字类型在内的多种隐式转换,如将字符串 ’07’ 转化为数字 7,而后进行比拟操作。
◦ Hive 隐式转换规则:详见链接 Allowed Implicit Conversions
• 尽管 Presto 也有本人的一套隐式类型转换规定蕴含在 public Optional<Type> 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: Unexpected parameters (varchar(1), decimal(2,1)) for function $operator$equal. Expected: $operator$equal(T, T) T:comparable