BigQuery“薛定谔行”或为什么 ROW_NUMBER() 不是一个好的标识符

Fab*_*ler 6 google-bigquery

情况

\n

我们有一个相当复杂的内部逻辑来将营销支出分配到各个渠道,并且目前已开始重新设计一些查询以简化设置。我们最近遇到了一个非常令人费解的案例,其中使用ROW_NUMBER() OVER()识别唯一行会导致非常奇怪的结果。

\n

问题

\n

本质上,使用ROW_NUMBER() OVER()导致了我所说的 Schr\xc3\xb6dingers 行。因为它们似乎同时匹配和不匹配(请在下面找到可复制的查询)。在所附的屏幕截图(这是查询的结果)中可以清楚地看到

\n

german_spend + non_german_spend > total_spend

\n

事实不应该是这样。

\n

在此输入图像描述

\n

询问

\n

请注意,每次运行查询时都会给出不同的结果,因为它依赖 RAND() 来生成虚拟数据。另请注意,该查询是我们正在做的事情的一个非常简单的版本。由于超出本文范围的原因,我们需要唯一地标识存储桶。

\n
###################\n# CREATE Dummy Data\n###################\nDECLARE NUMBER_OF_DUMMY_RECORDS DEFAULT 1000000;\n\nWITH data AS (\n  SELECT\n    num as campaign_id, \n    RAND() as rand_1,\n    RAND() as rand_2\n  FROM\n    UNNEST(GENERATE_ARRAY(1, NUMBER_OF_DUMMY_RECORDS)) AS num\n),\n\nspend_with_categories AS (\n  SELECT\n    campaign_id,\n    CASE \n      WHEN rand_1 < 0.25 THEN \'DE\'\n      WHEN rand_1 < 0.5 THEN \'AT\'\n      WHEN rand_1 < 0.75 THEN \'CH\'\n      ELSE \'IT\'\n    END AS country,\n    CASE \n      WHEN rand_2 < 0.25 THEN \'SMALL\'\n      WHEN rand_2 < 0.5 THEN \'MEDIUM\'\n      WHEN rand_2 < 0.75 THEN \'BIG\'\n      ELSE \'MEGA\'\n    END AS city_size,\n    CAST(RAND() * 1000000 AS INT64) as marketing_spend\n  FROM\n  data\n),\n###################\n# END Dummy Data\n###################\n\nspend_buckets AS (\n  SELECT\n    country,\n    city_size,\n    CONCAT("row_", ROW_NUMBER() OVER()) AS identifier,\n    #MD5(CONCAT(country, city_size)) AS identifier, (this works)\n    SUM(marketing_spend) AS marketing_spend\n  FROM \n    spend_with_categories\n  GROUP BY 1,2\n),\n\ngerman_spend AS (\n  SELECT\n    country,\n    ARRAY_AGG(identifier) AS identifier,\n    SUM(marketing_spend) AS marketing_spend\n  FROM \n    spend_buckets\n  WHERE\n    country = \'DE\'\n  GROUP BY\n    country\n),\n\ngerman_identifiers AS (\n  SELECT id AS identifier FROM german_spend, UNNEST(identifier) as id\n),\n\nnon_german_spend AS (\n  SELECT SUM(marketing_spend) AS marketing_spend FROM spend_buckets WHERE identifier NOT IN (SELECT identifier FROM german_identifiers)\n)\n\n(SELECT "german_spend" AS category, SUM(marketing_spend) AS marketing_spend FROM german_spend\nUNION ALL\nSELECT "non_german_spend" AS category, SUM(marketing_spend) AS marketing_spend FROM non_german_spend\nUNION ALL\nSELECT "total_spend" AS category, SUM(marketing_spend) AS marketing_spend FROM spend_buckets)\n
Run Code Online (Sandbox Code Playgroud)\n

解决方案

\n

我们实际上能够通过使用密钥的哈希值而不是密钥来解决问题ROW_NUMBER() OVER(),但出于好奇,我仍然很想了解导致此问题的原因。

\n

补充笔记

\n
    \n
  • 使用GENERATE_UUID() AS identifier代替CONCAT("row_", ROW_NUMBER() OVER()) AS identifier导致几乎 0 匹配。即全部支出被归类为非德国支出。

    \n
  • \n
  • 将 cost_buckets 写入表也可以解决问题,这让我相信也许ROW_NUMBER() OVER()是延迟执行的还是这样?

    \n
  • \n
  • 无论生成“唯一”id 的方法如何,使用较小的数字作为虚拟数据也会产生不匹配的结果

    \n
  • \n
\n

小智 1

哈希函数是一种比生成行号更好的标记行的方法,行号每天都在变化。

CTE(with表)不是持久的,而是针对查询中每次使用的时间进行计算。

在查询中多次运行相同的 CTE 会产生不同的结果:

With test as (Select rand() as x)

Select * from test
union all Select * from test
union all Select * from test
Run Code Online (Sandbox Code Playgroud)

一个好的解决方案是使用temp table. 解决方法是使用搜索 CTE 表,它会创建 row_number 或生成随机数,并在下面多次使用。这些CTE将被重命名并在递归CTE中使用,然后使用后面的CTE。在您的示例中,它是spend_buckets

WITH recursive
...
spend_buckets_ as (
...),
spend_buckets as
(select * from spend_buckets_
union all select * from spend_buckets_
where false
),
Run Code Online (Sandbox Code Playgroud)

然后值就会匹配。