【问题标题】:Build array based on add and remove event rows in BigQuery根据 BigQuery 中添加和删除事件行构建数组
【发布时间】:2018-07-25 10:04:23
【问题描述】:

我在 BigQuery 中有一个表,其结构如下:

datetime | event  | value
==========================
1        | add    | 1
---------+--------+-------
2        | remove | 1
---------+--------+-------
6        | add    | 2
---------+--------+-------
8        | add    | 3
---------+--------+-------
11       | add    | 4
---------+--------+-------
23       | remove | 3
---------+--------+-------

我正在尝试构建一个视图,该视图将list 列添加到包含数组当前状态的每一行。数组永远不会包含重复项。结果应该是这样的:

datetime | event  | value | list
===================================
1        | add    | 1     | [1]
---------+--------+-------+--------
2        | remove | 1     | []
---------+--------+-------+--------
6        | add    | 2     | [2]
---------+--------+-------+--------
8        | add    | 3     | [2,3]
---------+--------+-------+--------
11       | add    | 4     | [2,3,4]
---------+--------+-------+--------
23       | remove | 3     | [2,4]
---------+--------+-------+--------

我尝试使用分析函数,但没有成功。使用数组的 api 非常有限。如果我可以使用递归 WITH 子句,我想我会成功,不幸的是,这在 BigQuery 中是不可能的。

我正在使用启用了标准 SQL 的 BigQuery。

【问题讨论】:

  • 同一个数字可以多次出现吗?例如,如果3 有两个“添加”事件,3 有一个remove 事件,会发生什么?是否删除了所有出现的 3,还是只删除了一个?
  • @ElliottBrossard 这永远不会发生。

标签: sql google-cloud-platform google-bigquery bigquery-standard-sql


【解决方案1】:

以下版本适用于 BigQuery 标准 SQL,仅使用纯 SQL(无 JS UDF)

#standardSQL
WITH `project.dataset.events` AS (
  SELECT 1 dt,'add' event,'1' value UNION ALL
  SELECT 2,   'remove',   '1' UNION ALL
  SELECT 6,   'add',      '2' UNION ALL
  SELECT 8,   'add',      '3' UNION ALL
  SELECT 11,  'add',      '4' UNION ALL
  SELECT 23,  'remove',   '3' 
), cum AS (
  SELECT dt, event, value,
    SUM(IF(event = 'add', 1, -1)) OVER(PARTITION BY value ORDER BY dt) state
  FROM `project.dataset.events`
), pre AS (
  SELECT 
    a.dt, a.event, a.value, a.state, b.value AS b_value,
    ARRAY_AGG(b.state ORDER BY b.dt DESC)[SAFE_OFFSET(0)] b_state, 
    MAX(b.dt) b_dt 
  FROM cum a
  JOIN cum b ON b.dt <= a.dt
  GROUP BY a.dt, a.event, a.value, a.state, b.value
)
SELECT dt, event, value, 
  SPLIT(IFNULL(STRING_AGG(IF(b_state = 1, b_value, NULL) ORDER BY b_dt), '')) list_as_array,
  CONCAT('[', IFNULL(STRING_AGG(IF(b_state = 1, b_value, NULL) ORDER BY b_dt), ''), ']') list_as_string
FROM pre
GROUP BY dt, event, value
ORDER BY dt  

结果“令人惊讶”:o) 与我之前回答/发布的 JS UDF 版本完全相同

Row dt  event   value   list_as_arr list_as_string   
1   1   add     1       1           [1]  
2   2   remove  1                   []   
3   6   add     2       2           [2]  
4   8   add     3       2           [2,3]    
                        3        
5   11  add     4       2           [2,3,4]  
                        3        
                        4        
6   23  remove  3       2           [2,4]    
                        4   

注意:我认为上面的设计可能有点过度 - 但我只是没有时间可能完善/优化它 - 应该是可行的 - 把这个留给问题的所有者

【讨论】:

  • :D 看到基于 SQL 的解决方案非常酷。不是说 JavaScript 不好玩,而是感觉像是在作弊哈哈。
  • 很好,谢谢!我想知道使用unnestarray_agg 而不是string_aggsplit 的子查询/cte 会更好。有什么想法吗?
  • 纯 SQL 被认为是可取的,并且没有 js udf 相关的限制
【解决方案2】:

以下是 BigQuery SQL(可能是众多选项之一)

#standardSQL
CREATE TEMP FUNCTION CUST_ARRAY_AGG(arr ARRAY<STRUCT<event STRING, value STRING>>)
RETURNS ARRAY<STRING>
LANGUAGE js AS """
  var result = [];  
  for (i = 0; i < arr.length; i++) { 
    if (arr[i].event == 'add') {
      result.push(arr[i].value);
    } else {
      var index = result.indexOf(arr[i].value);
      if (index > -1) {
        result.splice(index, 1);
      }
    }
  }
  return result;
""";
WITH `project.dataset.events` AS (
  SELECT 1 dt, 'add' event, '1' value UNION ALL
  SELECT 2, 'remove', '1' UNION ALL
  SELECT 6, 'add', '2' UNION ALL
  SELECT 8, 'add', '3' UNION ALL
  SELECT 11, 'add', '4' UNION ALL
  SELECT 23, 'remove', '3' 
)
SELECT dt, event, value, 
  CUST_ARRAY_AGG(arr) list_as_arr,
  (SELECT CONCAT('[', IFNULL(STRING_AGG(item), ''), ']') FROM UNNEST(CUST_ARRAY_AGG(arr)) item) list_as_string
FROM (
  SELECT dt, event, value,
    ARRAY_AGG(STRUCT<event STRING, value STRING>(event, value)) OVER(ORDER BY dt) arr
  FROM `project.dataset.events`
)

结果如下

Row dt  event   value   list_as_arr list_as_string   
1   1   add     1       1           [1]  
2   2   remove  1                   []   
3   6   add     2       2           [2]  
4   8   add     3       2           [2,3]    
                        3        
5   11  add     4       2           [2,3,4]  
                        3        
                        4        
6   23  remove  3       2           [2,4]    
                        4   

【讨论】:

  • 不错!我仍在尝试提出一个仅使用 SQL 的版本...我觉得这一定是可能的。
  • @ElliottBrossard - 完全同意。应该是可能的。我从 js udf 开始,因为它更简单,更直接:o)我将在短途旅行之间尝试纯 sql - 我本周在加勒比海,海上的连接性不好:o)
  • @ElliottBrossard - 我想我明白了。见第二个答案:o)
【解决方案3】:

虽然已经解决了,但我喜欢这个问题。此解决方案的想法是首先使用窗口化获取完整历史记录,同时考虑所有前面的行 - 然后删除删除项:

#standardSQL
-- stolen test table ;)
WITH test AS (
  SELECT 1 dt,'add' event,'1' value UNION ALL
  SELECT 2,   'remove',   '1' UNION ALL
  SELECT 6,   'add',      '2' UNION ALL
  SELECT 8,   'add',      '3' UNION ALL
  SELECT 11,  'add',      '4' UNION ALL
  SELECT 23,  'remove',   '3' 
)

, windowing as (
SELECT *,
  -- add history using window function
  ARRAY_AGG(STRUCT(event, value)) OVER ( ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as history
FROM test)

SELECT 
  dt,
  event,
  value,
  --history, -- for testing

  -- Get all added items that are not removed items
  -- This sub-select runs within the row only, treating history-array as (sub-)table
  (SELECT ARRAY_AGG(value) value FROM unnest(t.history) l 
    WHERE l.event = 'add' 
    AND l.value NOT IN 
      (SELECT l.value FROM unnest(t.history) l WHERE l.event = 'remove')
  ) AS list
FROM windowing t

我没有比较性能,但会感兴趣!

【讨论】:

    【解决方案4】:

    所有原始答案都很棒(尤其是我的 - 大声笑),并且主要基于基于集合(处理大数据的最佳方式)处理,在这种情况下,对于非 sql 用户来说可能变得足够复杂!

    幸运的是,对 ScriptingStored Procedures 的支持现在处于测试阶段(截至 2019 年 10 月)

    您可以提交多个用分号分隔的语句,BigQuery 现在可以运行它们。以程序方式与基于集合的方式相比,更容易表达所需的逻辑(显然至少以性能为代价)——因此更多的用户可以受益

    下面的脚本实现了问题中表达的逻辑

    DECLARE arr ARRAY<STRUCT<dt INT64, event STRING, value STRING>>;
    DECLARE result ARRAY<STRUCT<dt INT64, event STRING, value STRING, list STRING>> DEFAULT [STRUCT(NULL, '', '', '')];
    DECLARE list ARRAY<STRING> DEFAULT [];
    DECLARE i, m INT64 DEFAULT -1; 
    
    SET arr = (
      WITH t AS (
        SELECT 1 dt,'add' event,'1' value UNION ALL
        SELECT 2,   'remove',   '1' UNION ALL
        SELECT 6,   'add',      '2' UNION ALL
        SELECT 8,   'add',      '3' UNION ALL
        SELECT 11,  'add',      '4' UNION ALL
        SELECT 23,  'remove',   '3' 
      )
      SELECT ARRAY_AGG(t) FROM t
    );
    
    SET m = ARRAY_LENGTH(arr);
    
    LOOP
      SET i = i + 1;
      IF i >= m THEN LEAVE;
      ELSE
        IF arr[OFFSET(i)].event = 'add' THEN 
          SET list = (
            SELECT ARRAY_CONCAT(list, [arr[OFFSET(i)].value])
          );    
        ELSE
          SET list = ARRAY(
            SELECT item
            FROM UNNEST(list) item 
            WHERE item != arr[OFFSET(i)].value
          );    
        END IF;
    
        SET result = (
          SELECT ARRAY_CONCAT(
            result, 
            [(arr[OFFSET(i)].dt, arr[OFFSET(i)].event, arr[OFFSET(i)].value, ARRAY_TO_STRING(list, ','))]
          )
        );
      END IF;
    END LOOP;
    
    SELECT * FROM UNNEST(result) WHERE NOT dt IS NULL;
    

    结果

    【讨论】:

      猜你喜欢
      • 2017-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-06
      • 1970-01-01
      • 2023-03-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多