假设我有一个模型,其中的A(t)
和B(t)
由以下等式控制:
A(t) = {
WHEN B(t-1) < 10 : B(t-1)
WHEN B(t-1) >=10 : B(t-1) / 6
}
B(t) = A(t) * 2
提供下表作为输入。
SELECT * FROM model ORDER BY t;
| t | A | B |
|---|------|------|
| 0 | 0 | 9 |
| 1 | null | null |
| 2 | null | null |
| 3 | null | null |
| 4 | null | null |
即我们知道A(t=0)
和B(t=0)
的值。
对于每一行,我们希望使用上述公式计算A
和B
的值。
决赛桌应该是:
| t | A | B |
|---|---|----|
| 0 | 0 | 9 |
| 1 | 9 | 18 |
| 2 | 3 | 6 |
| 3 | 6 | 12 |
| 4 | 2 | 4 |
我们尝试使用滞后,但是由于模型具有类似递归的性质,我们最终只能在A
处获得B
和(t=1)
CREATE TEMPORARY FUNCTION A_fn(b_prev FLOAT64) AS (
CASE
WHEN b_prev < 10 THEN b_prev
ELSE b_prev / 6.0
END
);
SELECT
t,
CASE WHEN t = 0 THEN A ELSE A_fn(LAG(B) OVER (ORDER BY t)) END AS A,
CASE WHEN t = 0 THEN B ELSE A_fn(LAG(B) OVER (ORDER BY t)) * 2 END AS B
FROM model
ORDER BY t;
产生:
| t | A | B |
|---|------|------|
| 0 | 0 | 9 |
| 1 | 9 | 18 |
| 2 | null | null |
| 3 | null | null |
| 4 | null | null |
每行均取决于其上方的行。在遍历各行的同时似乎应该可以一次计算一行。还是BigQuery不支持这种类型的窗口?
如果不可能,您有什么建议?
答案 0 :(得分:1)
第一回合-起点
以下内容适用于BigQuery Standard SQL,并且对我来说适用于最多3M行
#standardSQL
CREATE TEMP FUNCTION x(v FLOAT64, t INT64)
RETURNS ARRAY<STRUCT<t INT64, v FLOAT64>>
LANGUAGE js AS """
var i, result = [];
for (i = 1; i <= t; i++) {
if (v < 10) {v = 2 * v}
else {v = v / 3};
result.push({t:i, v});
};
return result
""";
SELECT 0 AS t, 0 AS A, 9 AS B UNION ALL
SELECT line.t, line.v / 2, line.v FROM UNNEST(x(9, 3000000)) line
在3M行以上产生Resources exceeded during query execution: UDF out of memory.
为了克服这个问题-我想您应该只在客户端上实现它-因此不应用JS UDF限制。我认为这是合理的“解决方法”,因为无论如何您似乎在BQ中都没有真正的数据,而只有一个起始值(在此示例中为9)。但是,即使表中确实有其他有价值的列-您也可以将产生的结果JOIN返回到表的t值-因此应该没问题!
第2轮-可能有数十亿……-因此,让我们关注规模,并行化
以下是避免JS UDF资源和/或内存错误的小技巧
因此,我一次就能在 2B行中运行它!
#standardSQL
CREATE TEMP FUNCTION anchor(seed FLOAT64, len INT64, batch INT64)
RETURNS ARRAY<STRUCT<t INT64, v FLOAT64>> LANGUAGE js AS """
var i, result = [], v = seed;
for (i = 0; i <= len; i++) {
if (v < 10) {v = 2 * v} else {v = v / 3};
if (i % batch == 0) {result.push({t:i + 1, v})};
}; return result
""";
CREATE TEMP FUNCTION x(value FLOAT64, start INT64, len INT64)
RETURNS ARRAY<STRUCT<t INT64, v FLOAT64>>
LANGUAGE js AS """
var i, result = []; result.push({t:0, v:value});
for (i = 1; i < len; i++) {
if (value < 10) {value = 2 * value} else {value = value / 3};
result.push({t:i, v:value});
}; return result
""";
CREATE OR REPLACE TABLE `project.dataset.result` AS
WITH settings AS (SELECT 9 init, 2000000000 len, 1000 batch),
anchors AS (SELECT line.* FROM settings, UNNEST(anchor(init, len, batch)) line)
SELECT 0 AS t, 0 AS A, init AS B FROM settings UNION ALL
SELECT a.t + line.t, line.v / 2, line.v
FROM settings, anchors a, UNNEST(x(v, t, batch)) line
在上面的查询中-您“控制”以下行中的初始值
WITH settings AS (SELECT 9 init, 2000000000 len, 1000 batch),
在上面的示例中,9
是初始值,2,000,000,000是要计算的行数,而1000是要处理的批次(这对于防止BQ Engine不会抛出资源和/或内存错误-您不能将其设置得太大或太小-我觉得我对它需要做什么有所了解-但不足以尝试制定它)
一些统计信息(设置-执行时间):
1M: SELECT 9 init, 1000000 len, 1000 batch - 0 min 9 sec
10M: SELECT 9 init, 10000000 len, 1000 batch - 0 min 50 sec
100M: SELECT 9 init, 100000000 len, 600 batch - 3 min 4 sec
100M: SELECT 9 init, 100000000 len, 40 batch - 2 min 56 sec
1B: SELECT 9 init, 1000000000 len, 10000 batch - 29 min 39 sec
1B: SELECT 9 init, 1000000000 len, 1000 batch - 27 min 50 sec
2B: SELECT 9 init, 2000000000 len, 1000 batch - 48 min 27 sec
第三回合-一些想法和评论
很明显,正如我在上面的#1中提到的-这种类型的计算更适合在您选择的客户上实施-因此,我很难判断上述的实用价值-但是我真的很开心玩它!实际上,我的脑子里没有更多有趣的想法,并且也付诸实践并发挥了作用-但在#2以上,它是最实用/可扩展的想法
注意:以上解决方案中最有趣的部分是anchors
表。生成非常便宜,并且可以按批量大小间隔设置锚点-因此有了它,例如,您可以计算row = 2,000,035或1,123,456,789(例如)的值,而无需实际处理所有先前的行-这将花费几分之一秒的时间。或者,您可以通过使用各自的锚点等启动多个线程/计算来并行化所有行的计算。有很多机会。
最后,这实际上取决于您的特定用例,哪种方法可以做得更好-因此,由您自己决定
答案 1 :(得分:0)
在遍历行的同时,似乎应该可以一次计算单个行
Scripting和Stored Procedures的支持现已处于测试阶段(截至2019年10月)
您可以提交多个用分号分隔的语句,BigQuery现在可以运行它们。
因此,从概念上讲,您的过程可能类似于以下脚本:
DECLARE b_prev FLOAT64 DEFAULT NULL;
DECLARE t INT64 DEFAULT 0;
DECLARE arr ARRAY<STRUCT<t INT64, a FLOAT64, b FLOAT64>> DEFAULT [STRUCT(0, 0.0, 9.0)];
SET b_prev = 9.0 / 2;
LOOP
SET (t, b_prev) = (t + 1, 2 * b_prev);
IF t >= 100 THEN LEAVE;
ELSE
SET b_prev = CASE WHEN b_prev < 10 THEN b_prev ELSE b_prev / 6.0 END;
SET arr = (SELECT ARRAY_CONCAT(arr, [(t, b_prev, 2 * b_prev)]));
END IF;
END LOOP;
SELECT * FROM UNNEST(arr);
即使上面的脚本更简单,更直接地代表了非技术人员的逻辑,并且更易于管理-如果您需要循环进行100次以上的迭代,则该脚本不适合方案。例如,上面的脚本花了将近2分钟,而我原来对100行的解决方案只花了2秒
但对于简单/较小的情况仍然很好