Как эффективно выбирать записи, соответствующие подстроке в другой таблице, используя BigQuery? - PullRequest
2 голосов
/ 05 июня 2019

У меня есть таблица из нескольких миллионов строк, которую я хочу сопоставить с таблицей из примерно двадцати тысяч строк, таких как:

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

К сожалению, это занимает ужасно много времени.

Учитывая, что таблица fragment содержит только 20 тыс. Записей, могу ли я загрузить ее в массив JavaScript с использованием UDF и сопоставить ее таким образом?Я пытаюсь понять, как это сделать прямо сейчас, но, возможно, уже есть какая-то магия, которую я мог бы сделать здесь, чтобы сделать это быстрее.Я попробовал 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 ]

2 голосов
/ 05 июня 2019

Ниже для BigQuery Standard SQL

#standardSQL
WITH record AS (
  SELECT LOWER(text) AS name
  FROM `bigquery-public-data.hacker_news.comments`
), fragment AS (
  SELECT DISTINCT LOWER(name) AS name
  FROM `bigquery-public-data.usa_names.usa_1910_current`
), temp_record AS (
  SELECT record, TO_JSON_STRING(record) id, name, item 
  FROM record, UNNEST(REGEXP_EXTRACT_ALL(name, r'\w+')) item 
), temp_fragment AS (
  SELECT name, item FROM fragment, UNNEST(REGEXP_EXTRACT_ALL(name, r'\w+')) item
)
SELECT AS VALUE ANY_VALUE(record) FROM (
  SELECT ANY_VALUE(record) record, id, r.name name, f.name fragment_name
  FROM temp_record r
  JOIN temp_fragment f
  USING(item)
  GROUP BY id, name, fragment_name
) 
WHERE name LIKE CONCAT('%', fragment_name, '%')
GROUP BY id   

выше было выполнено за 375 секунд, в то время как исходный запрос все еще выполняется 2740 секунд и продолжает работать, поэтому я даже не буду ждать его завершения

1 голос
/ 05 июня 2019

Ответ Михаила кажется более быстрым - но давайте найдем ответ, который не требует 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 секунд)

0 голосов
/ 05 июня 2019

Как было указано в моем вопросе, я работал над версией, использующей UDF JavaScript, которая решает эту задачу, хотя и медленнее, чем ответ, который я принял. Для полноты, я публикую это здесь, потому что, возможно, кто-то (как я в будущем) может найти это полезным.

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

Идея состоит в том, что список фрагментов достаточно мал, чтобы его можно было обрабатывать в массиве, аналогично ответу Фелипе с использованием регулярных выражений. Первое, что я делаю, это создаю таблицу fragment_array, сгруппированную по длинам фрагментов ... дешевый способ предотвращения массива слишком большого размера , который, как я обнаружил, , может вызвать тайм-ауты UDF.

Затем я создаю таблицу с именем records_with_fragments, которая соединяет эти массивы с исходными записями, находя только те, которые содержат соответствующий фрагмент, используя JavaScript UDF CONTAINS_ANY(). Это приведет к таблице, содержащей несколько дубликатов, поскольку одна запись может соответствовать нескольким фрагментам.

Последний SELECT затем извлекает исходную таблицу record, присоединяется к records_with_fragments, чтобы определить, какой фрагмент соответствует, а также использует функцию ROW_NUMBER() для предотвращения дублирования, например, показывает только первый ряд каждой записи, как однозначно идентифицируется ее name.

Теперь, причина, по которой я выполняю объединение в последнем запросе, заключается в том, что в моих фактических данных есть еще поля, которые я хочу, кроме сопоставляемой строки. Ранее в моих фактических данных я создал таблицу из DISTINCT строк, которые затем нужно будет снова объединить.

Вуаля! Не самый элегантный, но он выполняет свою работу.

...