Bigquery SQL:将数组转换为列

Bigquery SQL: convert array to columns

我有一个带有字段 A 的 table,其中每个条目都是固定长度的整数数组 A(比如长度 = 1000)。我想知道如何将它转换成1000列,列名由index_i给出,i=0,1,2,...,999,每个元素是对应的整数。我可以通过

之类的方式完成它
   A[OFFSET(0)] as index_0,
   A[OFFSET(1)] as index_1
   A[OFFSET(2)] as index_2,
   A[OFFSET(3)] as index_3,
   A[OFFSET(4)] as index_4,
    ...
   A[OFFSET(999)] as index_999,

我想知道这样做的优雅方式是什么。谢谢!

考虑以下方法

execute immediate ( select '''
select * except(id) from (
  select to_json_string(A) id, * except(A)
  from your_table, unnest(A) value with offset
)
pivot (any_value(value) index for offset in (''' 
|| (select string_agg('' || val order by offset) from unnest(generate_array(0,999)) val with offset) || '))'
)    

如果要应用于如下所示的虚拟数据(使用 10 个而不是 1000 个元素)

select [10,11,12,13,14,15,16,17,18,19] as A union all
select [20,21,22,23,24,25,26,27,28,29] as A union all
select [30,31,32,33,34,35,36,37,38,39] as A        

输出是

首先要说的是,遗憾的是,这将比大多数人预期的要复杂得多。从概念上讲,将值传递到脚本语言(例如 Python)并在那里工作可能更容易,但显然将内容保留在 BigQuery 中会提高性能。所以这里有一个方法。

Cross-joining 将数组字段变成 long-format tables

我认为您要做的第一件事是将值从数组中取出并放入行中。

通常在 BigQuery 中,这是使用 CROSS JOIN 完成的。语法有点不直观:

WITH raw AS (
    SELECT "A" AS name, [1,2,3,4,5] AS a
        UNION ALL
    SELECT "B" AS name, [5,4,3,2,1] AS a
),
long_format AS (
     SELECT name, vals
       FROM raw
 CROSS JOIN UNNEST(raw.a) AS vals
)

SELECT * FROM long_format

UNNEST(raw.a) 正在获取这些值数组并将每个数组转换为一组(五)行,然后将其中的每一行连接到 name 的相应值(定义CROSS JOIN)。这样我们就可以'unwrap'一个table带数组字段的

这将产生类似

的结果
 name | vals
-------------
   A  |  1  
   A  |  2  
   A  |  3  
   A  |  4  
   A  |  5  
   B  |  5  
   B  |  4  
   B  |  3  
   B  |  2  
   B  |  1  

令人困惑的是,此语法有一个 shorthand,其中 CROSS JOIN 被一个简单的逗号替换:

WITH raw AS (
    SELECT "A" AS name, [1,2,3,4,5] AS a
        UNION ALL
    SELECT "B" AS name, [5,4,3,2,1] AS a
),
long_format AS (
     SELECT name, vals
       FROM raw, UNNEST(raw.a) AS vals
)

SELECT * FROM long_format

这更紧凑,但如果您以前没有看过它,可能会感到困惑。

通常这是我们停止的地方。我们有一个 long-format table,创建时没有要求原始数组的长度都相同。你想要的东西更难产生 - 你想要一个 wide-format table 包含相同的信息(依赖于每个数组 相同的长度。

在 BigQuery 中转换 tables

好消息是 BigQuery 现在有一个 PIVOT 函数!这使得这种操作成为可能,尽管 non-trivial:

WITH raw AS (
    SELECT "A" AS name, [1,2,3,4,5] AS a
        UNION ALL
    SELECT "B" AS name, [5,4,3,2,1] AS a
),
long_format AS (
     SELECT name, vals, offset
       FROM raw, UNNEST(raw.a) AS vals WITH OFFSET
)

SELECT *
  FROM long_format PIVOT(
    ANY_VALUE(vals) AS vals 
    FOR offset IN (0,1,2,3,4)
)

这利用 WITH OFFSET 生成一个额外的 offset 列(以便我们知道数组中的值最初的顺序)。

此外,通常旋转需要我们聚合每个单元格中返回的值。但是这里我们期望名称和偏移量的每个组合 正好是一个 值,因此我们简单地使用聚合函数 ANY_VALUE,它 non-deterministically 从组中选择一个值你正在聚集。因为在这种情况下,每个组都有 恰好一个 值,这就是检索到的值。

查询产生的结果如下:

name    vals_0  vals_1  vals_2  vals_3  vals_4
----------------------------------------------  
 A        1       2       3       4       5
 B        5       4       3       2       1

这开始看起来不错,但我们有一个根本问题,因为列名仍然是 hard-coded。您希望它们动态生成。

不幸的是,PIVOT 无法接受数据透视列值的表达式 out-of-the-box。请注意,BigQuery 无法知道您的 long-format table 将整齐地解析为固定数量的列(它依赖于 offset 值为 0-4每组记录)。

动态地building/executing枢轴

然而,还是有办法的。我们将不得不离开标准 SQL 的舒适环境,进入 BigQuery 过程语言领域。

我们必须做的是使用表达式 EXECUTE IMMEDIATE,它允许我们动态构造和执行标准 SQL 查询!

(顺便说一句,我打赌你 - OP 或未来的搜索者 - 没想到这个兔子洞......)

当然,至少可以说这是不雅的。但这是上面的玩具示例,使用 EXECUTE IMMEDIATE 实现。诀窍是执行的查询被定义为一个字符串,所以我们只需要使用一个表达式来注入完整的运行你想要的值到这个字符串中。

回想一下,|| 可以用作字符串连接运算符。

EXECUTE IMMEDIATE """
WITH raw AS (
    SELECT "A" AS name, [1,2,3,4,5] AS a
        UNION ALL
    SELECT "B" AS name, [5,4,3,2,1] AS a
),
long_format AS (
     SELECT name, vals, offset
       FROM raw, UNNEST(raw.a) AS vals WITH OFFSET
)

SELECT *
  FROM long_format PIVOT(
    ANY_VALUE(vals) AS vals 
    FOR offset IN ("""
   || (SELECT STRING_AGG(CAST(x AS STRING)) FROM UNNEST(GENERATE_ARRAY(0,4)) AS x)
   || """
   )
)
"""

哎呀。我试图使它尽可能具有可读性。在底部附近有一个表达式生成足够的列列表(offset 的旋转值):

(SELECT STRING_AGG(CAST(x AS STRING)) FROM UNNEST(GENERATE_ARRAY(0,4)) AS x)

这会生成字符串 "0,1,2,3,4",然后将其连接起来在我们的最终查询中为我们提供 ...FOR offset IN (0,1,2,3,4)...(如之前的 hard-coded 示例所示)。

真正动态地执行枢轴

我没有逃过我的注意,这在技术上仍然坚持要求您知道 up-front 这些数组有多长!使用 GENERATE_ARRAY(0,4) 是一个很大的改进(在避免痛苦的重复代码的狭义意义上),但它不是 完全 所要求的。

遗憾的是,我无法提供可用的玩具示例,但我可以告诉您如何操作。您只需将枢轴值表达式替换为

(SELECT STRING_AGG(DISTINCT CAST(offset AS STRING)) FROM long_format)

但是在上面的例子中这样做 不会 工作,因为 long_format 是一个普通的 Table 表达式,它只是 EXECUTE IMMEDIATE 内定义。该块中的语句在构建它之前不会执行,因此在 build-time long_format 尚未定义。

然而一切并没有丢失。这会很好用:

SELECT *
  FROM d.long_format PIVOT(
    ANY_VALUE(vals) AS vals 
    FOR offset IN ("""
   || (SELECT STRING_AGG(DISTINCT CAST(offset AS STRING)) FROM d.long_format)
   || """
   )
)

...前提是您首先在数据集 d 中定义一个名为 long_format(或者更好的是一些更具表现力的名称)的 BigQuery VIEW(例如)。这样,构建查询的作业和运行它的作业都可以访问这些值。

如果成功,您应该看到两个作业都执行并成功。然后,您应该在 运行 查询的作业上单击 'VIEW RESULTS'。


最后,这假设您正在使用 BigQuery 控制台。如果您改为使用脚本语言工作,那么您将有很多选择来加载和操作数据,或者使用您的脚本语言构建查询,而不是让 BigQuery 为您做这件事。