如何引用列中的第一个非空字符串 - Cloudera Impala / Apache Hive / Spark SQL

Posted

技术标签:

【中文标题】如何引用列中的第一个非空字符串 - Cloudera Impala / Apache Hive / Spark SQL【英文标题】:How to Reference First Non-null String in a Column - Cloudera Impala / Apache Hive / Spark SQL 【发布时间】:2016-07-01 18:45:37 【问题描述】:

我正在使用 Impala SQL。我目前有一个包含 3 列的数据库:AccountDateType

Type 下有各种描述关联类型的数据字符串,但有些等于'UNKNOWN',有些等于null

我想创建另一个专栏Fixed_TypeFixed_Type 中的值应来自 Type 列。

如果Type 中的值是null'UNKNOWN',它应该在Type 列中获取最后一个有效值,按帐户分区并按日期排序。 如果分区以null'UNKNOWN' 开头,则Fixed_Type 中的值应该是Type 中的第一个有效值。

例如:

Account | Date | Type   |  Fixed_Type
1         Jan     data1     data1
1         Feb   'UNKNOWN'   data1
1         Mar     null      data1
2         Apr     data2     data2
2         May     null      data2
2         Jun     null      data2
2         Jul     data3     data3
3         Feb   'UNKNOWN'   data4
3         Mar   'UNKNOWN'   data4
3         Apr     data4     data4

我开始在 Oracle 中执行此操作,但后来意识到在 Impala 中没有实现类似于 IGNORE NULLS 的功能。

这就是我想在 Oracle 中做的事情(我意识到这只处理空值的前向填充):

select account, date, type, 
       case when type is null 
            then last_value(type ignore nulls)
                 over (partition by account order by date) 
            else type 
       end as fixed_type

【问题讨论】:

你有row_number() 吗?看起来很容易用自连接替换它。 是的,我确实有。 我猜你的日期是日期或数字,否则你不能通过文本订购 正确。它采用 Impala 日期格式,我可以根据需要进行转换以进行订购。 【参考方案1】:

我使用 postgresql 来测试查询,所以不能 100% 确定你是否可以让它在你的系统中工作。 WITH 可以替换为子查询。还必须将您的日期更改为数字,以便 ORDER BY 按预期工作。

enumerateWords:为有效单词创建一个枚举列表。 createFlag:设置一个标志,以便您可以验证下一个组何时开始。 createGrp :使用标志和SUM() 创建组。 最后你用枚举列表加入组分配Fixed_Type 当第一行是NULL'UNKNOWN' 时,JOIN c.grp = 0 and e.rn =1 中的特殊条件

Sql Fiddle Demo

WITH enumerateWords as (
    SELECT "Account", "Date", "Type",
           row_number() over (partition by "Account"
                              order by "Date") rn
    FROM Days  
    WHERE "Type" <> '''UNKNOWN''' AND "Type" IS NOT NULL   
),  createFlag as (
  SELECT *, CASE WHEN "Type" = '''UNKNOWN''' OR "Type" IS NULL 
                 THEN 0
                 ELSE 1
             END as FLAG       
  FROM Days  
), createGrp as ( 
  SELECT *,
         SUM(FLAG) OVER (PARTITION BY "Account" 
                         ORDER BY "Date") as grp         
  FROM createFlag  
)
SELECT c.*, e."Account", e."Date", e."Type" as "Fixed_Type"
FROM createGrp c
JOIN enumerateWords e
  ON c."Account" = e."Account"
 AND (     c.grp = e.rn   
       OR (c.grp = 0 and e.rn = 1)
     )

输出

您可以看到 createGrp 从 DB 上的值显示 Fixed_Type 类型,但 enumerateWords 从 Type 创建它。

您可以看到 flag 和 grp 如何协同工作以检测更改。

|                      createGrp                       ||      enumerateWords       |
|---------|------|-----------|------------|------|-----||---------|----|------------|
| Account | Date |      Type | Fixed_Type | flag | grp || Account | rn | Fixed_Type |
|---------|------|-----------|------------|------|-----||---------|----|------------|
|       1 |    1 |     data1 |      data1 |    1 |   1 ||       1 |  1 |      data1 |
|       1 |    2 | 'UNKNOWN' |      data1 |    0 |   1 ||       1 |  1 |      data1 |
|       1 |    3 |    (null) |      data1 |    0 |   1 ||       1 |  1 |      data1 |
|---------|------|-----------|------------|------|-----||---------|----|------------|
|       2 |    4 |     data2 |      data2 |    1 |   1 ||       2 |  1 |      data2 |
|       2 |    5 |    (null) |      data2 |    0 |   1 ||       2 |  1 |      data2 |
|       2 |    6 |    (null) |      data2 |    0 |   1 ||       2 |  1 |      data2 |
|       2 |    7 |     data3 |      data3 |    1 |   2 ||       2 |  2 |      data3 |
|       2 |    8 |    (null) |      data3 |    0 |   2 ||       2 |  2 |      data3 |
|---------|------|-----------|------------|------|-----||---------|----|------------|
|       3 |    9 | 'UNKNOWN' |      data4 |    0 |   0 ||       3 |  1 |      data4 | <= 
|       3 |   10 | 'UNKNOWN' |      data4 |    0 |   0 ||       3 |  1 |      data4 | <= 
|       3 |   11 |     data4 |      data4 |    1 |   1 ||       3 |  1 |      data4 |
                                                                    ^^^ special case 0 = 1

【讨论】:

谢谢@Juan。乍一看,这看起来很棒。我会根据我的环境进行调整,如果可行,我会立即接受您的回答。 祝你好运,我尽量让它更通用。【参考方案2】:

Oracle 设置

CREATE TABLE Table_Name ( Acct, Dt, Type ) AS
SELECT 1, DATE '2016-01-01', 'Data1'   FROM DUAL UNION ALL
SELECT 1, DATE '2016-02-01', 'UNKNOWN' FROM DUAL UNION ALL
SELECT 1, DATE '2016-03-01', NULL      FROM DUAL UNION ALL
SELECT 2, DATE '2016-04-01', 'Data2'   FROM DUAL UNION ALL
SELECT 2, DATE '2016-05-01', NULL      FROM DUAL UNION ALL
SELECT 2, DATE '2016-06-01', NULL      FROM DUAL UNION ALL
SELECT 2, DATE '2016-07-01', 'Data3'   FROM DUAL UNION ALL
SELECT 3, DATE '2016-02-01', 'UNKNOWN' FROM DUAL UNION ALL
SELECT 3, DATE '2016-03-01', 'UNKNOWN' FROM DUAL UNION ALL
SELECT 3, DATE '2016-04-01', 'Data4'   FROM DUAL;

查询

SELECT Acct,
       Dt,
       Type,
       Fixed_Type
FROM   (
  SELECT r.Acct,
         r.Dt,
         r.Type,
         t.type AS fixed_type,
         ROW_NUMBER() OVER ( PARTITION BY r.Acct, r.dt
                             ORDER BY SIGN( ABS( t.dt - r.dt ) ),
                                      SIGN( t.dt - r.dt ),
                                      ABS( t.dt - r.dt ) ) AS rn
  FROM   table_name r
         LEFT OUTER JOIN
         table_name t
         ON (    r.acct = t.acct
             AND t.type IS NOT NULL
             AND t.type <> 'UNKNOWN' )
)
WHERE   rn = 1
ORDER BY acct, dt;

解释

如果您将表连接到自身以使两个表具有相同的帐号,则您可以将每个帐户的每一行与同一帐户中的所有其他行进行比较。但是,我们不想比较所有行,而只是比较不是NULL'UNKNOWN' 的行,所以我们得到了连接条件:

ON (    r.acct = t.acct
    AND t.type IS NOT NULL
    AND t.type <> 'UNKNOWN' )

LEFT OUTER JOIN 用于以防万一帐号的类型包含所有 NULL'UNKNOWN' 值,以便不排除行。

然后是查找最近的行的问题。在 Oracle 中,如果你从另一个日期中减去一个日期,那么你会得到天数(或天数的分数)的差异 - 所以:

如果两个日期相同,SIGN( ABS( t.dt - r.dt ) ) 将给出0,如果它们不同,则给出1。按此顺序排序意味着如果有一个值具有相同的日期,那么它将优先于不同的日期; SIGN( t.dt - r.dt ) 将返回 0 如果两个日期相同(但已在前面的语句中过滤)或 -1 如果比较日期在当前行之前或 +1 如果它在之后 - 这是过去喜欢使用之前的日期而不是之后的日期。 ABS( t.dt - r.dt ) 将按最接近的日期排列日期。

所以ORDER BY 子句有效地声明:ORDER BY 相同的日期在前,然后是之前的日期(最接近r.dt),最后是之后的日期(最接近r.dt)。

然后将所有内容放在一个内联视图中并进行过滤以获得每一行的最佳匹配 (WHERE rn = 1)。

输出

      ACCT DT                  TYPE    FIXED_TYPE
---------- ------------------- ------- ----------
         1 2016-01-01 00:00:00 Data1   Data1      
         1 2016-02-01 00:00:00 UNKNOWN Data1      
         1 2016-03-01 00:00:00         Data1      
         2 2016-04-01 00:00:00 Data2   Data2      
         2 2016-05-01 00:00:00         Data2      
         2 2016-06-01 00:00:00         Data2      
         2 2016-07-01 00:00:00 Data3   Data3      
         3 2016-02-01 00:00:00 UNKNOWN Data4      
         3 2016-03-01 00:00:00 UNKNOWN Data4      
         3 2016-04-01 00:00:00 Data4   Data4      

【讨论】:

@Addie 解释已添加。【参考方案3】:

这是一个类似于 Juan Carlos 的解决方案,使用解析函数 countcase 表达式一次性创建组。

我创建了更多输入数据来测试,例如,当帐户只有 null 和/或 'UNKNOWN' 作为类型时会发生什么(确保左外连接按预期工作)。

create table table_name ( acct, dt, type ) as
select 1, date '2016-01-01', 'Data1'   from dual union all
select 1, date '2016-02-01', 'UNKNOWN' from dual union all
select 1, date '2016-03-01', null      from dual union all
select 2, date '2016-04-01', 'Data2'   from dual union all
select 2, date '2016-05-01', null      from dual union all
select 2, date '2016-06-01', null      from dual union all
select 2, date '2016-07-01', 'Data3'   from dual union all
select 3, date '2016-02-01', 'UNKNOWN' from dual union all
select 3, date '2016-03-01', 'UNKNOWN' from dual union all
select 3, date '2016-04-01', 'Data4'   from dual union all
select 3, date '2016-05-01', 'UNKNOWN' from dual union all
select 3, date '2016-06-01', 'Data5'   from dual union all
select 4, date '2016-02-01', null      from dual union all
select 4, date '2016-03-01', 'UNKNOWN' from dual;

SQL> select * from table_name;

      ACCT DT         TYPE
---------- ---------- -------
         1 2016-01-01 Data1
         1 2016-02-01 UNKNOWN
         1 2016-03-01
         2 2016-04-01 Data2
         2 2016-05-01
         2 2016-06-01
         2 2016-07-01 Data3
         3 2016-02-01 UNKNOWN
         3 2016-03-01 UNKNOWN
         3 2016-04-01 Data4
         3 2016-05-01 UNKNOWN
         3 2016-06-01 Data5
         4 2016-02-01
         4 2016-03-01 UNKNOWN

14 rows selected.

查询

with
     prep(acct, dt, type, gp) as (
       select acct, dt, type, 
              count(case when type != 'UNKNOWN' then 1 end)
                             over (partition by acct order by dt)
       from   table_name
     ),
     no_nulls(acct, type, gp) as (
       select acct, type, gp
       from   prep
       where  type != 'UNKNOWN'
     )
select p.acct, p.dt, p.type, n.type as fixed_type
from   prep p left outer join no_nulls n
on     p.acct = n.acct and (p.gp = n.gp or p.gp = 0 and n.gp = 1)
order by acct, dt;

输出

      ACCT DT         TYPE    FIXED_TYPE
---------- ---------- ------- ----------
         1 2016-01-01 Data1   Data1
         1 2016-02-01 UNKNOWN Data1
         1 2016-03-01         Data1
         2 2016-04-01 Data2   Data2
         2 2016-05-01         Data2
         2 2016-06-01         Data2
         2 2016-07-01 Data3   Data3
         3 2016-02-01 UNKNOWN Data4
         3 2016-03-01 UNKNOWN Data4
         3 2016-04-01 Data4   Data4
         3 2016-05-01 UNKNOWN Data4
         3 2016-06-01 Data5   Data5
         4 2016-02-01
         4 2016-03-01 UNKNOWN

14 rows selected.

【讨论】:

以上是关于如何引用列中的第一个非空字符串 - Cloudera Impala / Apache Hive / Spark SQL的主要内容,如果未能解决你的问题,请参考以下文章

MySQL中非空列中的空字符串?

如何在 Oracle 表的 varchar 列中的第二个和第四个字符之后插入“/”

如何使用AWK将包含特定字符串的行之后的行的第三列中的值打印到不同的文件?

如何替换 hive 列中的特殊字符?

粘贴值时如何引用非活动工作表?

通过引用字符串位置检查数据框列中的子字符串