作者:京東零售 李曉潔
我們常常忘記,天才也取決于其所能掌握的資料,即使阿基米德也無法設計出愛迪生的發明。——Ernest Dimnet
在大資料時代,精準而有效的資料對于每個緻力于長期發展的組織來說都是重要資産之一,而資料測試更是不可或缺的一部分。資料測試不僅關注資料加工的代碼邏輯,還要考慮大資料執行引擎帶來的影響,因為各種引擎架構将對同一份資料産生不同的計算或檢索結果。本文将從一個年度賬單bug引入,講解在資料測試實踐中對大資料執行引擎相容性差異的探索。
一、需求内容
京東-我的京東-年度賬單是一年一次,以使用者視角對在平台一年的消費情況進行總結。賬單從購物,權益,服務等方面切入,幫助使用者挖掘在自我難以認知的資料角度,通過這種方式讓使用者從賬單中發掘打動内心的立意,并主動進行分享和傳播。本次,我京年度賬單以“2022購物印象”為主題,通過不同的資料次元組成村落故事線,使用者以虛拟人物形象貫穿始終,使用者浏覽完故事線後,可生成購物印象。
年度賬單其中一個報表為使用者年度購買的小家電品類。該報表使用年度賬單彙總表中的小家電品類集合字段,計算了2022年度某使用者全年最後購買的兩款小家電所在的品類。本文bug分享将圍繞這個字段展開。
表名 | app_my_jd_user_bill_year_sum (使用者年度賬單彙總表) | |
字段名 | small_electrical_appliance_list 小家電品類集合 | |
資料類型 | String | |
資料描述 | 2022年中,使用者全年最後購買的兩款小家電所在的品類清單 | |
取數方式 | 按照下單時間倒序拍組,輸出全年最後購買的2個小家電品類;輸出的2個品類間用|分隔。 | |
資料源 | adm_my_jd_user_bill_month (使用者月度賬單明細表) |
二、 缺陷描述
缺陷描述:在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