【Hive笔记】 日常踩坑

隐形转换异常

  • Hive 版本为 1.2.1
  • 场景简述: 针对字符串字段与常量数值进行对比判断
  • 数据问题: 结果存在 null 情况

问题 SQL 举例如下:

SELECT 'default' <> -1

情景分析: SELECT 'default' <> -1 实际上相当于 SELECT CAST('default' AS INT) <> -1 由于左侧为 null 其结果也为 null

扩展思考

思考如下两种情况

SELECT 01 IN ('01')
-- 实际执行: SELECT CAST(01 AS STRING) IN ('01')
-- 请注意 01 会在分析阶段转换为 1
-- false
SELECT 01 = '01'
-- 实际执行: SELECT 01 = CAST('01' AS INT)
-- true

其实际原因在于 Hive 底层对于 IN= 不同的隐形转换优先级方式

比较函数的隐形转换分析

针对比较函数('=')其文件位置在 GenericUDFOPEqual.java

根据针对 GenericUDF 的了解, 其 initialize 函数是用于解析输入参数的类型和进行初始化操作, 该文件并没有, 因此需要向上回溯其父类 GenericUDFBaseCompare.java

针对 initialize 进行分析可以知道

  • 如果比较的两个变量类型相同, 即直接进行该类型的比较(设定 compareType 并在 GenericUDFOPEqual.java 中使用)
  • 如果比较的两个变量类型不同, 通过 FunctionRegistry.getCommonClassForComparison 进行 compareType 设定

仍需要向上追溯 FunctionRegistry.java, 可以找到最终的默认隐形转换代码:

public static TypeInfo getCommonClassForComparison(TypeInfo a, TypeInfo b) {
    // 前面基本逻辑: 若均为 String 族, 则使用 String 比较; 若互相为数值族和时间戳, 则采用 double 比较
    //
    for (PrimitiveCategory t : numericTypeList) {
        if (FunctionRegistry.implicitConvertible(pcA, t)
            && FunctionRegistry.implicitConvertible(pcB, t)) {
            return getTypeInfoForPrimitiveCategory((PrimitiveTypeInfo)a, (PrimitiveTypeInfo)b, t);
        }
    }
    //...
}

该部分则利用 implicitConvertible 判断若两者均可转为统一数值类型, 则进行该类型的转换(其中比较两者之间的优先级)

public static boolean implicitConvertible(PrimitiveCategory from, PrimitiveCategory to) {
    // 前面基本逻辑:
    // 允许从 String 转 Double, Decimal, Date
    // 允许数值族 转 String族

    // Allow implicit conversion from Byte -> Integer -> Long -> Float -> Double
    // Decimal -> String
    Integer f = numericTypes.get(from);
    Integer t = numericTypes.get(to);
    if (f == null || t == null) {
        return false;
    }
    if (f.intValue() > t.intValue()) {
        return false;
    }
    return true;
}

// 优先级部分的设置代码
static EnumMap<PrimitiveCategory, Integer> numericTypes =
    new EnumMap<PrimitiveCategory, Integer>(PrimitiveCategory.class);
static List<PrimitiveCategory> numericTypeList = new ArrayList<PrimitiveCategory>();

static void registerNumericType(PrimitiveCategory primitiveCategory, int level) {
    numericTypeList.add(primitiveCategory);
    numericTypes.put(primitiveCategory, level);
}

static {
    registerNumericType(PrimitiveCategory.BYTE, 1);
    registerNumericType(PrimitiveCategory.SHORT, 2);
    registerNumericType(PrimitiveCategory.INT, 3);
    registerNumericType(PrimitiveCategory.LONG, 4);
    registerNumericType(PrimitiveCategory.FLOAT, 5);
    registerNumericType(PrimitiveCategory.DOUBLE, 6);
    registerNumericType(PrimitiveCategory.DECIMAL, 7);
    registerNumericType(PrimitiveCategory.STRING, 8);
}

IN 函数的隐形转换分析

针对比较函数('IN')其文件位置在 GenericUDFIn.java, 可以看出该函数是自身含有 initialize 函数利用 GenericUDFUtils 进行类型的优先级匹配

因此向上回溯 GenericUDFUtils.java 其中利用 update 函数不断比较每个变量的类型情况, 关键代码如下

private boolean update(ObjectInspector oi, boolean isUnionAll) throws UDFArgumentTypeException {
    // ...
    TypeInfo commonTypeInfo = null;
    if (isUnionAll) {
    commonTypeInfo = FunctionRegistry.getCommonClassForUnionAll(oiTypeInfo,
        rTypeInfo);
    } else {    // 采用该情况, 因为无参数 update 默认 isunionall 为 false
    commonTypeInfo = FunctionRegistry.getCommonClass(oiTypeInfo,
        rTypeInfo);
    }
    if (commonTypeInfo == null) {
    return false;
    }
    // ...
}

因此又回到 FunctionRegistry.java文件, 其中 getCommonClass 函数调用 getCommonCategory 函数, 关键隐形转换代码如下

public static PrimitiveCategory getCommonCategory(TypeInfo a, TypeInfo b) {
    Integer ai = numericTypes.get(pcA);
    Integer bi = numericTypes.get(pcB);
    if (ai == null || bi == null) {
        // If either is not a numeric type, return null.
        return null;
    }

    return (ai > bi) ? pcA : pcB;
}

可以看出与比较的隐形转换相比, 该部分采用正常的向上优先级转换的情况.

总结

针对 HIVE 1.2.1 实际上存在三种隐形转换:

  • getCommonClass: 采用正常的优先级排序比较, 非比较类型基本均为该逻辑
  • getCommonClassForComparison: 在针对同族情况无效时, 采用向下比较优先级的方式(为防止用户使用出现异常错误), 非比较类型基本均为该逻辑
  • getCommonClassForUnionAll: 在存在一方非 String族 情况下, 无法实现相互转化情况下, 则采用数值向下比较优先级的方式(为防止用户使用出现异常错误)

LEFT JOIN 多个相同表问题

  • Hive 版本为 1.2.1 SPARK 版本 2.4.5 均可复现
  • 场景简述: 多个相同表取数, 并且 LEFT JOIN 到主表
  • 数据问题: 多个表仅会有一个表成功 JOIN 出对应数据

问题 SQL 举例如下:

SELECT
    KEY1,
    KEY2,
    NVL(VALUE1, 0) AS VALUE1,
    NVL(VALUE2, 0) AS VALUE2,
    NVL(VALUE3, 0) AS VALUE3
FROM (
    SELECT KEY1, KEY2
    FROM TABLE_1
) t1
LEFT JOIN (
    SELECT KEY1, KEY2, VALUE1
    FROM TABLE_2
    WHERE type = 'value1'
) t2
ON t1.KEY1 = t2.KEY1
AND t1.KEY2 = t2.KEY2
LEFT JOIN (
    SELECT KEY1, KEY2, VALUE2
    FROM TABLE_2
    WHERE type = 'value2'
) t3
ON t1.KEY1 = t3.KEY1
AND t1.KEY2 = t3.KEY2
LEFT JOIN (
    SELECT KEY1, KEY2, VALUE3
    FROM TABLE_2
    WHERE type = 'value3'
) t4
ON t1.KEY1 = t4.KEY1
AND t1.KEY2 = t4.KEY2

通过利用 EXPLAIN 可以看出针对 TABLE_2TableScan 会在同一个 StageMap Operator Tree 下, 并且可以看出最后一个 TableScan 即为成功获取到数据的 LEFT JOIN, 因此推断在实际构建数据时, 发生针对该多个 LEFT JOIN 的数据覆盖问题

解决方法: 利用 DISTINCT 从而拆分每个 LEFT JOIN 任务, 数据恢复正常

参考资料


【Hive笔记】 日常踩坑
https://www.windism.cn/2120316972.html
作者
windism
发布于
2022年2月22日
许可协议