如何使用BigQuery在另一个表中有效选择与子字符串匹配的记录?

时间:2019-06-04 22:11:56

标签: sql google-bigquery standard-sql

我有一个要与几万个字符串的表匹配的几百万个字符串的表,像这样:

#standardSQL
SELECT record.* FROM `record`
JOIN `fragment` ON record.name
  LIKE CONCAT('%', fragment.name, '%')

不幸的是,这花费了很长时间。

考虑到fragment表仅2万条记录,我可以使用UDF将其加载到JavaScript数组中并以这种方式进行匹配吗?我现在想弄清楚该如何做,但是也许我已经可以在这里做一些魔术来使它更快。我尝试了CROSS JOIN,但很快就超出了资源。我也尝试过使用EXISTS,但在该子查询的record.name中无法引用WHERE,而不会出错。

使用公共数据的示例

这似乎反映了相同数量的 数据...

#standardSQL
WITH record AS (
  SELECT LOWER(text) AS name
  FROM `bigquery-public-data.hacker_news.comments`
), fragment AS (
  SELECT LOWER(name) AS name, COUNT(*)
  FROM `bigquery-public-data.usa_names.usa_1910_current`
  GROUP BY name
)
SELECT record.* FROM `record`
JOIN `fragment` ON record.name
  LIKE CONCAT('%', fragment.name, '%')

3 个答案:

答案 0 :(得分:2)

以下是用于BigQuery标准SQL

ScaleQuantumToChar

以上操作在375秒内完成,而原始查询仍在2740秒处运行并保持运行,因此我什至不会等待它完成

答案 1 :(得分:1)

米哈伊尔(Mikhail)的答案似乎更快-但让我们有一个不需要SPLIT或将文本分成单词的

首先,使用所有要搜索的单词计算一个正则表达式:

#standardSQL
WITH record AS (
  SELECT text AS name
  FROM `bigquery-public-data.hacker_news.comments`
), fragment AS (
  SELECT name AS name, COUNT(*)
  FROM `bigquery-public-data.usa_names.usa_1910_current`
  GROUP BY name
)
SELECT FORMAT('(%s)',STRING_AGG(name,'|'))
FROM fragment

现在,您可以获取结果字符串,并在忽略大小写的REGEX中使用它:

#standardSQL
WITH record AS (
  SELECT text AS name
  FROM `bigquery-public-data.hacker_news.comments`
), largestring AS (
   SELECT '(?i)(mary|margaret|helen|more_names|more_names|more_names|josniel|khaiden|sergi)'
)

SELECT record.* FROM `record`
WHERE REGEXP_CONTAINS(record.name, (SELECT * FROM largestring))

(〜510秒)

答案 2 :(得分:0)

在我的问题中未曾提及,我使用JavaScript UDF开发了一个版本,该版本解决该问题的方式比我接受的答案要慢。为了完整起见,我将其发布在这里,因为也许有人(将来像我一样)可能会觉得有用。

CREATE TEMPORARY FUNCTION CONTAINS_ANY(str STRING, fragments ARRAY<STRING>)
RETURNS STRING
LANGUAGE js AS """
  for (var i in fragments) {
    if (str.indexOf(fragments[i]) >= 0) {
      return fragments[i];
    }
  }
  return null;
""";

WITH record AS (
  SELECT text AS name
  FROM `bigquery-public-data.hacker_news.comments`
  WHERE text IS NOT NULL
), fragment AS (
  SELECT name AS name, COUNT(*)
  FROM `bigquery-public-data.usa_names.usa_1910_current`
  WHERE name IS NOT NULL
  GROUP BY name
), fragment_array AS (
  SELECT ARRAY_AGG(name) AS names, COUNT(*) AS count
  FROM fragment
  GROUP BY LENGTH(name)
), records_with_fragments AS (
  SELECT record.name,
    CONTAINS_ANY(record.name, fragment_array.names)
      AS fragment_name
  FROM record INNER JOIN fragment_array
    ON CONTAINS_ANY(name, fragment_array.names) IS NOT NULL
)
SELECT * EXCEPT(rownum) FROM (
  SELECT record.name,
         records_with_fragments.fragment_name,
         ROW_NUMBER() OVER (PARTITION BY record.name) AS rownum
  FROM record
  INNER JOIN records_with_fragments
     ON records_with_fragments.name = record.name
    AND records_with_fragments.fragment_name IS NOT NULL
) WHERE rownum = 1

想法是片段列表相对较小,可以在数组中进行处理,类似于Felipe使用正则表达式的答案。我要做的第一件事是创建一个fragment_array表,该表按片段长度分组……一种防止超大型数组的便宜方法, 我发现这会导致UDF超时。 / p>

接下来,我创建一个名为records_with_fragments的表,该表将这些数组连接到原始记录,并使用JavaScript UDF CONTAINS_ANY()仅查找那些包含匹配片段的表。由于一条记录可能匹配多个片段,因此这将导致表中包含一些重复项。

最后的SELECT然后拉入原始的record表,连接到records_with_fragments以确定匹配的片段,还使用ROW_NUMBER()函数防止重复,例如仅显示由name唯一标识的每条记录的第一行。

现在,我在最终查询中进行联接的原因是,在我的实际数据中,除了要匹配的字符串外,我还需要其他字段。较早之前,在我的实际数据中,我创建了一个DISTINCT字符串表,随后需要将其重新连接。

Voila!不是最优雅的,但是可以完成工作。