最近用 Hive 实在太频繁了,此篇继续讲 Hive。
此篇遇到的问题是要以某几列为 key,对 Hive SQL SELECT 出来的数据进行去重。以下逐步讨论。
DISTINCT
说到要去重,自然会想到 DISTINCT。但是在 Hive SQL 里,它有两个问题。
DISTINCT会以SELECT出的全部列作为 key 进行去重。也就是说,只要有一列的数据不同,DISTINCT就认为是不同数据而保留。DISTINCT会将全部数据打到一个 reducer 上执行,造成严重的数据倾斜,耗时巨大。
ROW_NUMBER() OVER
DISTINCT 的两个问题,用 ROW_NUMBER() OVER 可解。比如,如果我们要按 key1 和 key2 两列为 key 去重,就会写出这样的代码:
1 | WITH temp_table AS ( |
这样,Hive 会按 key1 和 key2 为 key,将数据打到不同的 mapper 上,然后对 key1 和 key2 都相同的一组数据,按 column 升序排列,并最终在每组中保留排列后的第一条数据。借此就完成了按 key1 和 key2 两列为 key 的去重任务。
注意 PARTITION BY 在此起到的作用:一是按 key1 和 key2 打散数据,解决上述问题 (2);二是与 ORDER BY 和 rn = 1 的条件结合,按 key1 和 key2 对数据进行分组去重,解决上述问题 (1)。
但显然,这样做十分不优雅(not-elegant),并且不难想见其效率比较低。
GROUP BY 和 COLLECT_SET/COLLECT_LIST
ROW_NUMBER() OVER 的解法的一个核心是利用 PARTITION BY 对数据按 key 分组,同样的功能用 GROUP BY 也可以实现。但是,GROUP BY 需要与聚合函数搭配使用。ORDER BY 和 rn = 1 的条件结合起来实现了「保留第一条」的功能。我们需要考虑,什么样的聚合函数能实现或者间接实现这样的功能呢?不难想到有 COLLECT_SET 和 COLLECT_LIST。
于是有这样的代码:
1 | SELECT |
对于 key1 和 key2 以外的列,我们用 COLLECT_LIST 将他们收集起来,然后输出第一个收集进来的结果。这里使用 COLLECT_LIST 而非 COLLECT_SET 的原因在于 SET 内是无序的,因此你无法保证输出的 columns 都来自同一条数据。若对于此没有要求或限制,则可以使用 COLLECT_SET,它会更节省资源。
相比前一种办法,由于省略了排序和(可能的)落盘动作,所以效率会高不少。但是因为(可能)不落盘,所以 COLLECT_LIST 中的数据都会缓存在内存当中。如果重复数量特别大,这种方法可能会触发 OOM。此时应考虑将数据进一步打散,然后再合并;或者干脆换用前一种办法。