搜尋已建立索引的資料

在 BigQuery 中搜尋已建立索引的資料,可提升查詢效能,同時盡可能減少掃描的資料量。您可以對已建立索引的資料表使用 SEARCH 函式和支援的運算子,盡可能減少處理的位元組和運算單元毫秒數,尤其是在擷取總資料列的一小部分時。

建立搜尋索引

下表名為 Logs,用於顯示 SEARCH 函式的不同使用方式。這個範例資料表相當小,但在實際情況下,SEARCH 帶來的效能提升會隨著資料表的大小而增加。

CREATE TABLE my_dataset.Logs (Level STRING, Source STRING, Message STRING)
AS (
  SELECT 'INFO' as Level, '65.177.8.234' as Source, 'Entry Foo-Bar created' as Message
  UNION ALL
  SELECT 'WARNING', '132.249.240.10', 'Entry Foo-Bar already exists, created by 65.177.8.234'
  UNION ALL
  SELECT 'INFO', '94.60.64.181', 'Entry Foo-Bar deleted'
  UNION ALL
  SELECT 'SEVERE', '4.113.82.10', 'Entry Foo-Bar does not exist, deleted by 94.60.64.181'
  UNION ALL
  SELECT 'INFO', '181.94.60.64', 'Entry Foo-Baz created'
);

資料表如下所示:

+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 65.177.8.234   | Entry Foo-Bar created                                 |
| WARNING | 132.249.240.10 | Entry Foo-Bar already exists, created by 65.177.8.234 |
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
| INFO    | 181.94.60.64   | Entry Foo-Baz created                                 |
+---------+----------------+-------------------------------------------------------+

使用預設文字分析器,在 Logs 資料表建立搜尋索引:

CREATE SEARCH INDEX my_index ON my_dataset.Logs(ALL COLUMNS);

如要進一步瞭解搜尋索引,請參閱「管理搜尋索引」。

使用 SEARCH 函式

SEARCH 函式可對資料執行權杖化搜尋。SEARCH 應與索引搭配使用,以最佳化查詢。您可以使用 SEARCH 函式搜尋整個資料表,或將搜尋範圍限制在特定資料欄。

搜尋整個資料表

下列查詢會搜尋 Logs 資料表的所有資料欄,找出 bar 值,並傳回包含這個值的資料列,不論大小寫。由於搜尋索引使用預設文字分析器,因此您不需要在 SEARCH 函式中指定。

SELECT * FROM my_dataset.Logs WHERE SEARCH(Logs, 'bar');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 65.177.8.234   | Entry Foo-Bar created                                 |
| WARNING | 132.249.240.10 | Entry Foo-Bar already exists, created by 65.177.8.234 |
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

下列查詢會在 Logs 資料表的所有資料欄中搜尋 `94.60.64.181` 值,並傳回包含這個值的資料列。反引號可進行精確搜尋,因此 Logs 資料表中包含 181.94.60.64 的最後一列會遭到省略。

SELECT * FROM my_dataset.Logs WHERE SEARCH(Logs, '`94.60.64.181`');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

搜尋部分資料欄

SEARCH 可讓您輕鬆指定要在哪些資料欄子集中搜尋資料。下列查詢會在 Logs 資料表的 Message 資料欄中搜尋 94.60.64.181 值,並傳回含有這個值的資料列。

SELECT * FROM my_dataset.Logs WHERE SEARCH(Message, '`94.60.64.181`');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

下列查詢會同時搜尋 Logs 資料表的 Source 和 Message 欄。並傳回任一欄中包含 94.60.64.181 值的資料列。

SELECT * FROM my_dataset.Logs WHERE SEARCH((Source, Message), '`94.60.64.181`');
+---------+----------------+-------------------------------------------------------+
| Level   | Source         | Message                                               |
+---------+----------------+-------------------------------------------------------+
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                                 |
| SEVERE  | 4.113.82.10    | Entry Foo-Bar does not exist, deleted by 94.60.64.181 |
+---------+----------------+-------------------------------------------------------+

如果資料表有多個資料欄,而您想搜尋其中大部分的資料欄,則可以只指定要從搜尋中排除的資料欄,這樣可能比較容易。下列查詢會搜尋 Logs 資料表的所有資料欄,但 Message 資料欄除外。並傳回 Message 以外任何資料欄中包含 94.60.64.181 值的資料列。

SELECT *
FROM my_dataset.Logs
WHERE SEARCH(
  (SELECT AS STRUCT Logs.* EXCEPT (Message)), '`94.60.64.181`');
+---------+----------------+---------------------------------------------------+
| Level   | Source         | Message                                           |
+---------+----------------+---------------------------------------------------+
| INFO    | 94.60.64.181   | Entry Foo-Bar deleted                             |
+---------+----------------+---------------------------------------------------+

使用其他文字分析器

下列範例會建立名為 contact_info 的資料表,並使用 NO_OP_ANALYZER 文字分析器建立索引:

CREATE TABLE my_dataset.contact_info (name STRING, email STRING)
AS (
  SELECT 'Kim Lee' AS name, 'kim.lee@example.com' AS email
  UNION ALL
  SELECT 'Kim' AS name, 'kim@example.com' AS email
  UNION ALL
  SELECT 'Sasha' AS name, 'sasha@example.com' AS email
);
CREATE SEARCH INDEX noop_index ON my_dataset.contact_info(ALL COLUMNS)
OPTIONS (analyzer = 'NO_OP_ANALYZER');
+---------+---------------------+
| name    | email               |
+---------+---------------------+
| Kim Lee | kim.lee@example.com |
| Kim     | kim@example.com     |
| Sasha   | sasha@example.com   |
+---------+---------------------+

下列查詢會在 name 欄中搜尋 Kim,並在 email 欄中搜尋 kim。由於搜尋索引不使用預設文字分析器,因此您必須將分析器的名稱傳遞至 SEARCH 函式。

SELECT
  name,
  SEARCH(name, 'Kim', analyzer=>'NO_OP_ANALYZER') AS name_Kim,
  email,
  SEARCH(email, 'kim', analyzer=>'NO_OP_ANALYZER') AS email_kim
FROM
  my_dataset.contact_info;

NO_OP_ANALYZER 不會修改文字,因此 SEARCH 函式只會針對完全比對的項目傳回 TRUE:

+---------+----------+---------------------+-----------+
| name    | name_Kim | email               | email_kim |
+---------+----------+---------------------+-----------+
| Kim Lee | FALSE    | kim.lee@example.com | FALSE     |
| Kim     | TRUE     | kim@example.com     | FALSE     |
| Sasha   | FALSE    | sasha@example.com   | FALSE     |
+---------+----------+---------------------+-----------+

設定文字分析器選項

如要自訂 LOG_ANALYZER 和 PATTERN_ANALYZER 文字分析器,請在設定選項中加入 JSON 格式的字串。您可以在 SEARCH 函式、CREATE SEARCH INDEX DDL 陳述式和 TEXT_ANALYZE 函式中設定文字分析器。

以下範例會建立名為 complex_table 的資料表,並使用 LOG_ANALYZER 文字分析器建立索引。它會使用 JSON 格式的字串來設定分析器選項:

CREATE TABLE dataset.complex_table(
  a STRING,
  my_struct STRUCT<string_field STRING, int_field INT64>,
  b ARRAY<STRING>
);

CREATE SEARCH INDEX my_index
ON dataset.complex_table(a, my_struct, b)
OPTIONS (analyzer = 'LOG_ANALYZER', analyzer_options = '''{
  "token_filters": [
    {
      "normalizer": {"mode": "NONE"}
    }
  ]
}''');

下表列出使用不同文字分析器呼叫 SEARCH 函式的範例及其結果。第一個資料表使用預設文字分析器 LOG_ANALYZER 呼叫 SEARCH 函式:

函式呼叫 傳回 原因
SEARCH('foobarexample', NULL) 錯誤 search_terms 為 `NULL`。
SEARCH('foobarexample', '') 錯誤 search_terms 不含任何權杖。
SEARCH('foobar-example', 'foobar example') TRUE 「-」和「 」是分隔符號。
SEARCH('foobar-example', 'foobarexample') FALSE search_terms 未分割。
SEARCH('foobar-example', 'foobar\\&example') TRUE 雙反斜線會逸出做為分隔符號的和號。
SEARCH('foobar-example', R'foobar\&example') TRUE 單一反斜線會逸出原始字串中的和號。
SEARCH('foobar-example', '`foobar&example`') FALSE 反引號需要與 foobar&example 完全比對。
SEARCH('foobar&example', '`foobar&example`') TRUE 找到完全比對的項目。
SEARCH('foobar-example', 'example foobar') TRUE 字詞的順序不會造成任何影響。
SEARCH('foobar-example', 'foobar example') TRUE 權杖會轉換為小寫。
SEARCH('foobar-example', '`foobar-example`') TRUE 找到完全比對的項目。
SEARCH('foobar-example', '`foobar`') FALSE 反引號會保留大小寫。
SEARCH('`foobar-example`', '`foobar-example`') FALSE 反引號對 data_to_search 和
SEARCH('foobar@example.com', '`example.com`') TRUE 在 data_to_search 的分隔符號後方找到完全比對的項目。
SEARCH('a foobar-example b', '`foobar-example`') TRUE 空格分隔符號之間有完全比對的項目。
SEARCH(['foobar', 'example'], 'foobar example') FALSE 沒有任何陣列項目符合所有搜尋字詞。
SEARCH('foobar=', '`foobar\\=`') FALSE search_terms 相當於 foobar\=。
SEARCH('foobar=', R'`foobar\=`') FALSE 這與前一個示例相同。
SEARCH('foobar=', 'foobar\\=') TRUE 等號是資料和查詢中的分隔符號。
SEARCH('foobar=', R'foobar\=') TRUE 這與前一個示例相同。
SEARCH('foobar.example', '`foobar`') TRUE 找到完全比對的項目。
SEARCH('foobar.example', '`foobar.`') FALSE `foobar.` 由於反引號而未分析;
SEARCH('foobar..example', '`foobar.`') TRUE `foobar.` 因反引號而未分析,

下表列出使用 NO_OP_ANALYZER 文字分析器呼叫 SEARCH 函式的範例,以及各種傳回值的原因:

函式呼叫 退貨 原因
SEARCH('foobar', 'foobar', analyzer=>'NO_OP_ANALYZER') TRUE 找到完全比對的項目。
SEARCH('foobar', '`foobar`', analyzer=>'NO_OP_ANALYZER') FALSE 反引號不是 NO_OP_ANALYZER 的特殊字元。
SEARCH('Foobar', 'foobar', analyzer=>'NO_OP_ANALYZER') FALSE 大小寫不符。
SEARCH('foobar example', 'foobar', analyzer=>'NO_OP_ANALYZER') FALSE NO_OP_ANALYZER 沒有分隔符。
SEARCH('', '', analyzer=>'NO_OP_ANALYZER') TRUE NO_OP_ANALYZER 沒有分隔符。

其他運算子和函式

您可以使用多個運算子、函式和述詞,執行搜尋索引最佳化。

使用運算子和比較函式進行最佳化

BigQuery 可最佳化部分查詢,這些查詢使用等號運算子 (=)、IN 運算子、LIKE 運算子、STARTS_WITH 函式或 ENDS_WITH 函式,比較字串常值與索引資料。

使用字串述詞進行最佳化

下列述詞符合搜尋索引最佳化條件:

  • column_name = 'string_literal'
  • 'string_literal' = column_name
  • struct_column.nested_field = 'string_literal'
  • string_array_column[OFFSET(0)] = 'string_literal'
  • string_array_column[ORDINAL(1)] = 'string_literal'
  • column_name IN ('string_literal1', 'string_literal2', ...)
  • column_name LIKE 'prefix%'
  • STARTS_WITH(column_name, 'prefix')
  • ENDS_WITH(column_name, 'some suffix')

使用數值述詞進行最佳化

如果搜尋索引是以數值資料類型建立,BigQuery 可以最佳化部分查詢,這些查詢會使用等號運算子 (=) 或 IN 運算子搭配索引資料。下列述詞符合搜尋索引最佳化條件:

  • INT64(json_column.int64_field) = 1
  • int64_column = 1
  • int64_array_column[OFFSET(0)] = 1
  • int64_column IN (1, 2)
  • struct_column.nested_int64_field = 1
  • struct_column.nested_timestamp_field = TIMESTAMP "2024-02-15 21:31:40"
  • timestamp_column = "2024-02-15 21:31:40"
  • timestamp_column IN ("2024-02-15 21:31:40", "2024-02-16 21:31:40")

最佳化產生索引資料的函式

對已建立索引的資料套用特定函式時,BigQuery 支援搜尋索引最佳化。如果搜尋索引使用預設的 LOG_ANALYZER 文字分析器,則可將 UPPER 或 LOWER 函式套用至資料欄,例如 UPPER(column_name) = 'STRING_LITERAL'。

對於從索引 JSON 資料欄擷取的 JSON 純量字串資料,您可以套用 STRING 函式或其安全版本 SAFE.STRING。如果擷取的 JSON 值不是字串,STRING 函式會產生錯誤,SAFE.STRING 函式則會傳回 NULL。

對於已建立索引的 JSON 格式 STRING (而非 JSON) 資料,您可以套用下列函式:

舉例來說,假設您有下列名為 dataset.person_data 的索引資料表,其中包含 JSON 和 STRING 資料欄:

+----------------------------------------------------------------+-----------------------------------------+
| json_column                                                    | string_column                           |
+----------------------------------------------------------------+-----------------------------------------+
| { "name" : "Ariel", "email" : "cloudysanfrancisco@gmail.com" } | { "name" : "Ariel", "job" : "doctor" }  |
+----------------------------------------------------------------+-----------------------------------------+

下列查詢符合最佳化條件:

SELECT * FROM dataset.person_data
WHERE SAFE.STRING(json_column.email) = 'cloudysanfrancisco@gmail.com';
SELECT * FROM dataset.person_data
WHERE JSON_VALUE(string_column, '$.job') IN ('doctor', 'lawyer', 'teacher');

這些函式的組合也經過最佳化,例如 UPPER(JSON_VALUE(json_string_expression)) = 'FOO'。

使用 JSON_FLATTEN 進行最佳化

BigQuery 支援搜尋索引最佳化,適用於使用 JSON_FLATTEN 函式展開 JSON 陣列的查詢,通常與 EXISTS 和 UNNEST 搭配使用。

舉例來說,假設您有一個名為 dataset.logs 的資料表,其中包含 JSON 資料欄 json_payload。下列查詢符合最佳化資格:

SELECT json_payload
FROM dataset.logs
WHERE EXISTS(
  SELECT 1
  FROM UNNEST(JSON_FLATTEN(JSON_QUERY(json_payload, "lax recursive $.message"))) AS f
  WHERE SEARCH(f, "nullpointerexception")
);

搜尋索引使用情形

如要判斷查詢是否使用搜尋索引,您可以查看工作詳細資料,或查詢其中一個 INFORMATION_SCHEMA.JOBS* 檢視表。

查看工作詳細資料

在「Query results」(查詢結果) 的「Job Information」(工作資訊) 中,「Index Usage Mode」(索引使用模式) 和「Index Unused Reasons」(未使用索引的原因) 欄位會提供搜尋索引使用情形的詳細資訊。

工作資訊,說明搜尋索引未使用的原因。

您也可以透過 Jobs.Get API 方法中的 searchStatistics 欄位,取得搜尋索引使用情形的相關資訊。searchStatistics 中的 indexUsageMode 欄位會指出是否使用了搜尋索引,並提供下列值:

  • UNUSED:未使用搜尋索引。
  • PARTIALLY_USED:部分查詢使用搜尋索引,部分則未使用。
  • FULLY_USED:查詢中的每個 SEARCH 函式都使用了搜尋索引。

如果 indexUsageMode 為 UNUSED 或 PARTIALLY_USED,indexUnusedReasons 欄位會包含查詢未採用搜尋索引的原因。

如要查看查詢的 searchStatistics,請執行 bq show 指令。

bq show --format=prettyjson -j JOB_ID

範例

假設您執行查詢,對資料表中的資料呼叫 SEARCH 函式。您可以查看查詢的工作詳細資料,找出工作 ID,然後執行 bq show 指令,查看更多資訊:

bq show --format=prettyjson --j my_project:US.bquijob_123x456_789y123z456c

輸出內容包含許多欄位,其中 searchStatistics 欄位看起來會像下方這樣。在這個範例中,indexUsageMode 表示未使用索引。這是因為資料表沒有搜尋索引。如要解決這個問題,請在資料表上建立搜尋索引。如要查看查詢可能不使用搜尋索引的所有原因清單,請參閱 indexUnusedReason code 欄位。

"searchStatistics": {
  "indexUnusedReasons": [
    {
      "baseTable": {
        "datasetId": "my_dataset",
        "projectId": "my_project",
        "tableId": "my_table"
      },
      "code": "INDEX_CONFIG_NOT_AVAILABLE",
      "message": "There is no search index configuration for the base table `my_project:my_dataset.my_table`."
    }
  ],
  "indexUsageMode": "UNUSED"
},

查詢 INFORMATION_SCHEMA 檢視表

您也可以在下列檢視畫面中,查看某個區域內多項工作的搜尋索引使用情形:

下列查詢會顯示過去 7 天內,所有可最佳化搜尋索引查詢的索引使用情形資訊:

SELECT
  job_id, search_statistics.index_usage_mode, index_unused_reason.code, index_unused_reason.base_table.table_id, index_unused_reason.index_name
FROM
  `region-REGION_NAME`.INFORMATION_SCHEMA.JOBS, UNNEST(search_statistics.index_unused_reasons) AS index_unused_reason
WHERE
  end_time BETWEEN TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY) AND CURRENT_TIMESTAMP();

結果大致如下:

+-----------+----------------------------------------+-----------------------+
| job_id    | index_usage_mode | code                | table_id | index_name |
+-----------+------------------+---------------------+-----------------------+
| bquxjob_1 | UNUSED           | BASE_TABLE_TOO_SMALL| my_table | my_index   |
| bquxjob_2 | FULLY_USED       | NULL                | my_table | my_index   |
+-----------+----------------------------------------+-----------------------+

最佳做法

以下各節將說明搜尋的最佳做法。

選擇性搜尋

如果搜尋結果很少,搜尋功能的效果最好。盡可能具體說明搜尋內容。

ORDER BY LIMIT 最佳化

在非常大的分區資料表上,使用 SEARCH、=、IN、LIKE 或 STARTS_WITH 的查詢,可透過在分區欄位上使用 ORDER BY 子句和 LIMIT 子句來最佳化。如果查詢不含 SEARCH 函式,您可以使用其他運算子和函式,充分運用最佳化功能。無論資料表是否已建立索引,系統都會套用最佳化。如果搜尋的是常見字詞,這個方法很實用。舉例來說,假設先前建立的 Logs 資料表,是依名為 day 的額外 DATE 已指定類型和格式的欄分區。以下查詢已最佳化:

SELECT
  Level, Source, Message
FROM
  my_dataset.Logs
WHERE
  SEARCH(Message, "foo")
ORDER BY
  day
LIMIT 10;

使用 SEARCH 函式時,請只搜尋預期含有搜尋字詞的資料表資料欄。這可提升效能,並減少需要掃描的位元組數。

使用反引號

搭配使用 SEARCH 函式和 LOG_ANALYZER 文字分析器時,如果用反引號括住搜尋查詢,系統會強制執行完全比對。如果搜尋內容區分大小寫,或包含不應解讀為分隔符號的字元,這項功能就非常實用。例如,如要搜尋 IP 位址 192.0.2.1,請使用 `192.0.2.1`。如果沒有反引號,搜尋會傳回包含個別符記 192、0、2 和 1 的任何資料列,順序不限。