【问题标题】:bigquery SQL to calculate the Start and finish times for a project schedulebigquery SQL 计算项目计划的开始和结束时间
【发布时间】:2018-01-09 14:23:24
【问题描述】:
ID  NAME    DURATION    START   FINISH  P1  P2  P3
1   A         14                         1      
2   B         15                         1      
3   C         15                         1      
4   D         12                         1      
5   E         22                         2  3   
6   F         14                         4  1   
7   G         9                          5  6   

需要开发一个递归大查询 SQL 来计算计划的开始和结束时间。
请注意,活动的日程安排数量可能会有所不同
P1 、 P2 和 P3 是行活动的前身 ID
一个活动可能只有 1 个 Predecessor 或多个 Predecessor
开始和结束列的计算如下
Start 等于 "2017-01-01" 或 (Max (Finish of its Predecessors +1 )) 的较大值
完成始终是开始 + 持续时间的函数 -1
该计算预计将是递归的,直到获得答案。

我在 bigquery 中需要这个的原因是通过更改持续时间值的迭代来执行计划模拟。
上述时间表的答案如下:

ID  NAME    DURATION    START       FINISH      P1  P2  P3
1   A       14          1-Jan-17    14-Jan-17   1       
2   B       15          15-Jan-17   29-Jan-17   1       
3   C       15          15-Jan-17   29-Jan-17   1       
4   D       12          15-Jan-17   26-Jan-17   1       
5   E       22          30-Jan-17   20-Feb-17   2   3   
6   F       14          27-Jan-17   9-Feb-17    4   1   
7   G       9           21-Feb-17   1-Mar-17    5   6   

【问题讨论】:

    标签: mysql sql google-bigquery


    【解决方案1】:

    我希望通过一次查询进行迭代。是否可以使用内联 JS ...?

    以下是 BigQuery 标准 SQL
    它使用JS UDF 在一次运行中完成所有迭代 这需要将整个表的数据传递给 UDF,因此绝对是 UDF limits / limitations 的主题 我怀疑这个解决方案是否具有真正的实用价值,但绝对有趣的是锻炼和探索未来的 BigQuery 功能

    还要注意这里为简化 JS 代码和关注问题根源所做的一些假设。所以假设是:所有 id 字段都连续填充,没有从值 1 开始的间隙(如果你愿意,你可以改进它:o))

    #standardSQL
    CREATE TEMPORARY FUNCTION y(arr ARRAY<STRING>)
    RETURNS ARRAY<STRUCT<id INT64, name STRING, duration INT64, start INT64, finish INT64, p1 INT64, p2 INT64, p3 INT64>>
    LANGUAGE js AS """
      var result = [], fin;
      for (i = 0; i < arr.length; i++){result.push(JSON.parse(arr[i]))}
      for (w = 0; w < 40; w++){ fin = true;
        for (i = 0; i < arr.length; i++) {
          if(result[i].start == null) { fin = false;
            var x1 = parseInt(result[i].p1) - 1;
            var x2 = parseInt(result[i].p2||result[i].p1) - 1;
            var x3 = parseInt(result[i].p3||result[i].p1) - 1;
            if(result[i].id == result[i].p1) {
                result[i].start = 1; 
                result[i].finish = 1 + result[i].duration - 1;
            } else if (result[x1].start !== null && result[x2].start !== null && result[x3].start !== null) {
                result[i].start = Math.max(result[x1].finish, result[x2].finish, result[x3].finish) + 1;
                result[i].finish = result[i].start + result[i].duration - 1;
            } 
          }
        } if (fin) {return result}
      } return result;
    """;
    SELECT 
      id, name, duration, 
      DATE_ADD(DATE '2017-01-01', INTERVAL start - 1 DAY) start, 
      DATE_ADD(DATE '2017-01-01', INTERVAL finish - 1 DAY) finish, 
      p1, p2, p3 
    FROM (
      SELECT rec.* FROM (
        SELECT ARRAY_AGG(TO_JSON_STRING(t) ORDER BY id) AS data
        FROM `yourTable` t
      ), UNNEST(y(data)) AS rec
    ) ORDER BY id
    

    您可以使用下面的虚拟数据(来自您的问题)测试/玩上面的内容

    #standardSQL
    CREATE TEMPORARY FUNCTION y(arr ARRAY<STRING>)
    RETURNS ARRAY<STRUCT<id INT64, name STRING, duration INT64, start INT64, finish INT64, p1 INT64, p2 INT64, p3 INT64>>
    LANGUAGE js AS """
      var result = [], fin;
      for (i = 0; i < arr.length; i++){result.push(JSON.parse(arr[i]))}
      for (w = 0; w < 40; w++){ fin = true;
        for (i = 0; i < arr.length; i++) {
          if(result[i].start == null) { fin = false;
            var x1 = parseInt(result[i].p1) - 1;
            var x2 = parseInt(result[i].p2||result[i].p1) - 1;
            var x3 = parseInt(result[i].p3||result[i].p1) - 1;
            if(result[i].id == result[i].p1) {
                result[i].start = 1; 
                result[i].finish = 1 + result[i].duration - 1;
            } else if (result[x1].start !== null && result[x2].start !== null && result[x3].start !== null) {
                result[i].start = Math.max(result[x1].finish, result[x2].finish, result[x3].finish) + 1;
                result[i].finish = result[i].start + result[i].duration - 1;
            } 
          }
        } if (fin) {return result}
      } return result;
    """;
    WITH `yourTable` AS (SELECT * FROM (
      SELECT NULL id, NULL name, NULL duration, 1 start, 1 finish, 1 p1, 1 p2, 1 p3 UNION ALL
      SELECT 1,    'A',      14,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 2,    'B',      15,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 3,    'C',      15,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 4,    'D',      12,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 5,    'E',      22,          NULL,       NULL,        2,    3,       NULL    UNION ALL   
      SELECT 6,    'F',      14,          NULL,       NULL,        4,    1,       NULL    UNION ALL   
      SELECT 7,    'G',      9,           NULL,       NULL,        5,    6,       NULL   
      ) WHERE NOT id IS NULL
    )
    SELECT 
      id, name, duration, 
      DATE_ADD(DATE '2017-01-01', INTERVAL start - 1 DAY) start, 
      DATE_ADD(DATE '2017-01-01', INTERVAL finish - 1 DAY) finish, 
      p1, p2, p3 
    FROM (
      SELECT rec.* FROM (
        SELECT ARRAY_AGG(TO_JSON_STRING(t) ORDER BY id) AS data
        FROM `yourTable` t
      ), UNNEST(y(data)) AS rec
    ) ORDER BY id  
    

    结果是

    id  name    duration    start       finish      p1      p2      p3   
    1   A       14          2017-01-01  2017-01-14  1       null    null     
    2   B       15          2017-01-15  2017-01-29  1       null    null     
    3   C       15          2017-01-15  2017-01-29  1       null    null     
    4   D       12          2017-01-15  2017-01-26  1       null    null     
    5   E       22          2017-01-30  2017-02-20  2       3       null     
    6   F       14          2017-01-27  2017-02-09  4       1       null     
    7   G       9           2017-02-21  2017-03-01  5       6       null     
    

    【讨论】:

    • 感谢 Mikhail Berlyant 的回答。我已尝试进行一些更改,请参阅下面的答案。我收到 SyntaxError: Unexpected token for at y(ARRAY) line 4, columns 2-5。我不确定我的回答是否会使查询更有效率。需要你的建议。
    • 谢谢米哈伊尔。你的回答很聪明
    【解决方案2】:

    BigQuery 不支持递归查询
    因此,您需要使用您选择的client 自己编排递归

    下面是如何通过运行一系列查询直到解决所有依赖项来实现这一点的演示

    第 1 步:准备初始表格 - yourproject.yourdataset.yourtable(此处取自您的问题的简单示例)

    第 2 步:使用与源表相同的目标表运行迭代查询 - yourproject.yourdataset.yourtable with Write Preference >> Overwrite table

    #standardSQL
    SELECT a.id, a.name, a.duration, a.p1, a.p2, a.p3,
      start,
      DATE_ADD(start, INTERVAL a.duration - 1 DAY) finish
    FROM `yourproject.yourdataset.yourtable` a
    LEFT JOIN `yourproject.yourdataset.yourtable` a1 ON a.p1 = a1.id
    LEFT JOIN `yourproject.yourdataset.yourtable` a2 ON a.p2 = a2.id
    LEFT JOIN `yourproject.yourdataset.yourtable` a3 ON a.p3 = a3.id
    CROSS JOIN UNNEST([
      IF(a.id = a.p1, DATE '2017-01-01', ((
        SELECT DATE_ADD(MAX(finish), INTERVAL 1 DAY) 
        FROM UNNEST([a1.finish, a2.finish, a3.finish]) finish
        WHERE IF(a.p1 IS NULL, 0, 1) + IF(a.p2 IS NULL, 0, 1) + IF(a.p3 IS NULL, 0, 1) = 
        IF(a1.finish IS NULL, 0, 1) + IF(a2.finish IS NULL, 0, 1) + IF(a3.finish IS NULL, 0, 1)
      )))   
    ]) start
    WHERE NOT a.id IS NULL
    ORDER BY a.id
    

    第 3 步:检查是否还有要计算的条目

    #standardSQL
    SELECT COUNT(1) still_to_iterate 
    FROM `yourproject.yourdataset.yourtable`
    WHERE start IS NULL   
    

    如果此处的计数大于 0 – 继续执行步骤 2,依此类推,直到 still_to_iterate = 0

    处理示例:

    如果您手动执行这些步骤 - 以下是您得到的结果

    迭代 1

    迭代 2

    迭代 3

    迭代 4

    当然,对于更实际的情况 - 迭代次数可能会很高,即使仍然可以手动完成,但很快就会变得不高效!
    这就是您可以使用您选择的clientbq command line 以及一些 bash / awk / 等魔法来编写上述逻辑的地方

    【讨论】:

    • 感谢您的回答。但我希望的是通过一次查询进行迭代。是否可以使用内联JS创建可以自行迭代的临时表,而不是与源表同名的目标表?
    • 正如我提到的 - BigQuery does not support recursive query,所以上面是您拥有的最佳选择
    • 我发布了另一个答案(第一个和第二个相对较大,所以我将第二个作为单独的) - 看看 - 玩得开心 :o)
    【解决方案3】:
    #standardSQL
    CREATE TEMPORARY FUNCTION y(arr ARRAY<STRING>)
    RETURNS ARRAY<STRUCT<id INT64, name STRING, duration INT64, start INT64, finish INT64, p1 INT64, p2 INT64, p3 INT64>>
    LANGUAGE js AS """
      var result = [],
    
      for (var i = 0; i < arr.length; i++){result.push(JSON.parse(arr[i]))}
    
      ----------
        result[1].finish= new Date(new Date('01-Jan-17').getTime()- 1 * 86400000)
    
      for(var i=1;i<arr.length;i++){
    
    if (result[i].p1 === ''){result[i].p1 = result[1].id}
    if (result[i].p2 === ''){result[i].p2 = result[1].id}
    if (result[i].p3 === ''){result[i].p3 = result[1].id}
      }
      for(var i=1;i<arr.length;i++){
    
        result[i].start= new Date(Math.max(  new Date('01-Jan-17').getTime() 
                                                    ,Math.max(result[result[i][result[i].p1]].finish.getTime() + 1 * 86400000 ,
                                                              result[result[i][result[i].p2]].finish.getTime() + 1 * 86400000 ,
                                                              result[result[i][result[i].p3]].finish.getTime() + 1 * 86400000 )
                                                              )
                                                              )
    
    
       result[i].finish=  new Date( result[i].start.getTime() + result[i].duration * 86400000 - 1 * 86400000 )
       } 
    
       return result;
    """;
    WITH `yourTable` AS (SELECT * FROM (
      SELECT NULL id, NULL name, NULL duration, 1 start, 1 finish, 1 p1, 1 p2, 1 p3 UNION ALL
      SELECT 1,    'A',      14,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 2,    'B',      15,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 3,    'C',      15,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 4,    'D',      12,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
      SELECT 5,    'E',      22,          NULL,       NULL,        2,    3,       NULL    UNION ALL   
      SELECT 6,    'F',      14,          NULL,       NULL,        4,    1,       NULL    UNION ALL   
      SELECT 7,    'G',      9,           NULL,       NULL,        5,    6,       NULL   
      ) WHERE NOT id IS NULL
    )
    SELECT 
      id, name, duration, 
       start, 
       finish, 
      p1, p2, p3 
    FROM (
      SELECT rec.* FROM (
        SELECT ARRAY_AGG(TO_JSON_STRING(t) ORDER BY id) AS data
        FROM `yourTable` t
      ), UNNEST(y(data)) AS rec
    ) ORDER BY id 
    

    【讨论】:

      【解决方案4】:

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

      您可以提交多个用分号分隔的语句,BigQuery 现在可以运行它们。

      我希望通过一次查询进行迭代

      所以,现在您可以将所需的逻辑实现为一个纯 SQL 脚本(不涉及 JS UDF 且无需手动迭代),如下例所示

      DECLARE cnt INT64;
      CREATE TEMP TABLE temp_table AS SELECT * FROM (
        SELECT NULL id, NULL name, NULL duration, CURRENT_DATE() start, CURRENT_DATE() finish, 1 p1, 1 p2, 1 p3 UNION ALL
        SELECT 1,    'A',      14,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
        SELECT 2,    'B',      15,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
        SELECT 3,    'C',      15,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
        SELECT 4,    'D',      12,          NULL,       NULL,        1,    NULL,    NULL    UNION ALL
        SELECT 5,    'E',      22,          NULL,       NULL,        2,    3,       NULL    UNION ALL   
        SELECT 6,    'F',      14,          NULL,       NULL,        4,    1,       NULL    UNION ALL   
        SELECT 7,    'G',      9,           NULL,       NULL,        5,    6,       NULL   
        ) WHERE NOT id IS NULL;
      
      LOOP
        CREATE OR REPLACE TEMP TABLE temp_table AS 
        SELECT a.id, a.name, a.duration, a.p1, a.p2, a.p3,
          start, DATE_ADD(start, INTERVAL a.duration - 1 DAY) finish
        FROM temp_table a
        LEFT JOIN temp_table a1 ON a.p1 = a1.id
        LEFT JOIN temp_table a2 ON a.p2 = a2.id
        LEFT JOIN temp_table a3 ON a.p3 = a3.id
        CROSS JOIN UNNEST([
          IF(a.id = a.p1, DATE '2017-01-01', ((
            SELECT DATE_ADD(MAX(finish), INTERVAL 1 DAY) 
            FROM UNNEST([a1.finish, a2.finish, a3.finish]) finish
            WHERE IF(a.p1 IS NULL, 0, 1) + IF(a.p2 IS NULL, 0, 1) + IF(a.p3 IS NULL, 0, 1) = 
            IF(a1.finish IS NULL, 0, 1) + IF(a2.finish IS NULL, 0, 1) + IF(a3.finish IS NULL, 0, 1)
          )))   
        ]) start
        WHERE NOT a.id IS NULL;
      
        SET cnt = (SELECT COUNT(1) FROM temp_table WHERE start IS NULL);
        IF cnt = 0 THEN BREAK; END IF; 
      END LOOP;
      
      SELECT * FROM temp_table ORDER BY id;  
      

      上面的脚本最终执行了 12 个作业 - 一个父作业和 11 个子作业

      如果你要检查最终工作的结果 - 你会看到结果表

      【讨论】: