Bigtable 適用的 GoogleSQL 總覽

您可以使用 GoogleSQL 陳述式查詢 Bigtable 資料。GoogleSQL 是符合 ANSI 標準的結構化查詢語言 (SQL),也適用於其他服務,例如 BigQuery 和 Spanner。 Google Cloud

本文概要介紹 Bigtable 適用的 GoogleSQL。這份文件提供可搭配 Bigtable 使用的 SQL 查詢範例,並說明這些查詢與 Bigtable 資料表結構定義的關係。閱讀本文之前,請先熟悉 Bigtable 儲存空間模型結構定義設計概念

您可以在 Google Cloud 控制台的 Bigtable Studio 中建立及執行查詢,也可以使用 Bigtable Java、Python 或 Go 用戶端程式庫Python,以程式輔助方式執行查詢。詳情請參閱「使用 Bigtable 用戶端程式庫搭配 SQL」。

叢集節點處理 SQL 查詢的方式與 NoSQL 資料要求相同。因此,建立要對 Bigtable 資料執行的 SQL 查詢時,請遵循相同的最佳做法,例如避免完整資料表掃描或複雜的篩選條件。詳情請參閱「讀取和效能」。

您無法將 Data Boost 與 Bigtable 的 GoogleSQL 搭配使用。

用途

GoogleSQL for Bigtable 非常適合開發低延遲應用程式。此外,在Google Cloud 控制台中執行 SQL 查詢,有助於快速取得資料表結構定義的視覺化表示法、驗證特定資料是否已寫入,或偵錯可能的資料問題。

目前 Bigtable 適用的 GoogleSQL 版本不支援部分常見的 SQL 建構函式,包括但不限於下列項目:

  • SELECT 以外的資料操縱語言 (DML) 陳述式,例如 INSERTUPDATEDELETE
  • 資料定義語言 (DDL) 陳述式,例如 CREATEALTERDROP
  • 資料存取權控管陳述式
  • 子查詢、JOINUNIONCTEs 的查詢語法

如要進一步瞭解支援的函式、運算子、資料型別和查詢語法,請參閱 Bigtable 適用的 GoogleSQL 參考文件

瀏覽次數

您可以使用 Bigtable 適用的 GoogleSQL 建立下列資源:

如要比較這些類型的檢視畫面和授權檢視畫面,請參閱「表格和檢視畫面」。

基本概念

本節將討論使用 GoogleSQL 查詢 Bigtable 資料時,需要注意的重要概念。

SQL 回應中的資料欄系列

在 Bigtable 中,資料表包含一或多個資料欄系列,用於將資料欄分組。使用 GoogleSQL 查詢 Bigtable 資料表時,資料表的結構定義包含下列項目:

  • 名為 _key 的特殊資料欄,對應於所查詢資料表中的資料列鍵
  • 資料表中每個 Bigtable 資料欄系列各有一個資料欄,其中包含該資料列的資料欄系列資料

地圖資料類型

Bigtable 適用的 GoogleSQL 包含 MAP<key, value> 資料型別,專門用於容納資料欄系列。

根據預設,對應資料欄中的每個資料列都包含鍵/值配對,其中鍵是所查詢資料表中的 Bigtable 資料欄限定符,值則是該資料欄的最新值。

以下是 SQL 查詢範例,會傳回資料表,其中包含資料列鍵值和來自名為 columnFamily 的對應項的限定符最新值。

  SELECT _key, columnFamily['qualifier'] FROM myTable

如果 Bigtable 結構定義涉及在資料欄中儲存多個儲存格或資料版本,您可以在 SQL 陳述式中加入時間篩選器,例如 with_history

在此情況下,代表資料欄系列的對應會巢狀化,並以陣列形式傳回。在陣列中,每個值本身都是對應項,由時間戳記 (做為鍵) 和儲存格資料 (做為值) 組成。格式為 MAP<key, ARRAY<STRUCT<timestamp, value>>>

以下範例會傳回單一資料列中「info」資料欄系列的所有儲存格。

  SELECT _key, info FROM users(with_history => TRUE) WHERE _key = 'user_123';

傳回的地圖如下所示。在查詢的資料表中,info 是資料欄系列,user_123 是資料列索引鍵,citystate 則是資料欄限定詞。陣列中的每個時間戳記值配對 (STRUCT) 代表該列中這些資料欄的儲存格,並依時間戳記降序排序。

/*----------+------------------------------------------------------------------+
 |   _key   |                              info                                |
 +----------+------------------------------------------------------------------+
 | user_123 | {"city":{<t5>:"Brooklyn", <t0>:"New York"}, "state":{<t0>:"NY"}} |
 +----------+------------------------------------------------------------------*/

稀疏資料表

Bigtable 的主要特色是彈性資料模型。在 Bigtable 資料表中,如果資料列未使用某個資料欄,系統就不會儲存該資料欄的資料。某列可能只有一欄,下一列則可能有 100 欄。相較之下,關聯式資料庫資料表中的所有資料列都包含所有資料欄,而 NULL 值通常會儲存在沒有該資料欄資料的資料列資料欄中。

不過,使用 GoogleSQL 查詢 Bigtable 資料表時,未使用的資料欄會以空白對應表示,並以 NULL 值傳回。這些 NULL 值可用做查詢述詞。舉例來說,WHERE family['column1'] IS NOT NULL 等述詞可用於只在資料列中使用 column1 時傳回資料列。

位元組

提供字串時,GoogleSQL 預設會從 STRING 值隱含轉換為 BYTES 值。舉例來說,這表示您可以提供字串 'qualifier',而不是位元組序列 b'qualifier'

由於 Bigtable 預設會將所有資料視為位元組,因此大多數 Bigtable 資料欄都不含類型資訊。不過,您可以使用 GoogleSQL 的 CAST 函式,在讀取時定義結構定義。如要進一步瞭解轉換,請參閱轉換函式

時間篩選器

下表列出存取表格時間元素時可使用的引數。引數會按照篩選順序列出。舉例來說,with_history 會在 latest_n 之前套用。請務必提供有效時間戳記。

引數 說明
as_of 時間戳記。傳回時間戳記小於或等於所提供時間戳記的最新值。
with_history Boolean。控制是否要以純量形式傳回最新值,或以 STRUCT 形式傳回含時間戳記的值。
after 時間戳記。時間戳記晚於輸入時間的值 (不含輸入時間)。需要「with_history => TRUE」欄。
after_or_equal 時間戳記。時間戳記晚於輸入時間的值 (含輸入時間)。需要「with_history => TRUE」欄。
before 時間戳記。時間戳記早於輸入的時間 (不含該時間) 的值。需要「with_history => TRUE」欄。
latest_n 整數。每個資料欄要傳回的時間戳記值數量 限定符 (對應鍵)。不得小於 1。需要 with_history => TRUE

如需更多範例,請參閱進階查詢模式

基礎查詢

本節將說明基本的 Bigtable SQL 查詢,並提供範例,如需其他查詢範例,請參閱 GoogleSQL for Bigtable 查詢模式範例

擷取最新版本

雖然 Bigtable 可讓您在每個資料欄中儲存多個版本的資料,但 GoogleSQL for Bigtable 預設會傳回每個資料列的最新版本資料 (即最近的儲存格)。

請看以下範例資料集,其中顯示 user1 在紐約州境內搬遷兩次,在布魯克林市內搬遷一次。在本例中,address 是資料欄系列,而資料欄限定詞是 streetcitystate。資料欄中的儲存格會以空白行分隔。

address
_key street city state
user1 2023/01/10-14:10:01.000:
'113 Xyz Street'

2021/12/20-09:44:31.010:
'76 Xyz Street'

2005/03/01-11:12:15.112:
'123 Abc Street'
2021/12/20-09:44:31.010:
'Brooklyn'

2005/03/01-11:12:15.112:
'Queens'
2005/03/01-11:12:15.112:
'NY'

如要擷取 user1 中每個資料欄的最新版本,可以使用類似下列的 SELECT 陳述式。

   SELECT address['street'], address['city'] FROM myTable WHERE _key = 'user1'

回覆內容包含目前地址,也就是以 JSON 格式列印的最新街道、城市和州值 (在不同時間寫入)。回覆中不會顯示時間戳記。

_key address
user1 {street:'113 Xyz Street', city:'Brooklyn', state: :'NY'}

擷取所有版本

如要擷取資料的舊版本 (儲存格),請使用 with_history 旗標。您也可以為資料欄和運算式設定別名,如以下範例所示。

  SELECT _key, columnFamily['qualifier'] AS col1
  FROM myTable(with_history => TRUE)

如要進一步瞭解導致資料列目前狀態的事件,您可以擷取完整記錄,取得每個值的時間戳記。舉例來說,如要瞭解 user1 何時搬到目前地址,以及搬遷前的地址,可以執行下列查詢:

  SELECT
    address['street'][0].value AS moved_to,
    address['street'][1].value AS moved_from,
    FORMAT_TIMESTAMP('%Y-%m-%d', address['street'][0].timestamp) AS moved_on,
  FROM myTable(with_history => TRUE)
  WHERE _key = 'user1'

在 SQL 查詢中使用 with_history 標記時,系統會以 MAP<key, ARRAY<STRUCT<timestamp, value>>> 形式傳回回應。陣列中的每個項目都是指定資料列、資料欄系列和資料欄的時間戳記值。時間戳記會依時間倒序排列,因此傳回的第一個項目一律是最新資料。

查詢回應如下。

moved_to moved_from moved_on
113 Xyz Street 76 Xyz Street 2023/01/10

您也可以使用陣列函式,擷取每個資料列中的版本數量,如下列查詢所示:

  SELECT _key, ARRAY_LENGTH(MAP_ENTRIES(address)) AS version_count
  FROM myTable(with_history => TRUE)

從指定時間擷取資料

使用 as_of 篩選器可擷取特定時間點的資料列狀態。舉例來說,如要查詢 user 在 2022 年 1 月 10 日下午 1:14 的地址,可以執行下列查詢。

  SELECT address
  FROM myTable(as_of => TIMESTAMP('2022-01-10T13:14:00.234Z'))
  WHERE _key = 'user1'

結果會顯示 2022 年 1 月 10 日下午 1:14 的最後已知地址,也就是 2021 年 12 月 20 日上午 9:44:31.010 更新中的街道和城市,以及 2005 年 3 月 1 日上午 11:12:15.112 更新中的州別。

address
{street:'76 Xyz Street', city:'Brooklyn', state: :'NY'}

使用 Unix 時間戳記也能達到相同效果。

  SELECT address
  FROM myTable(as_of => TIMESTAMP_FROM_UNIX_MILLIS(1641820440000))
  WHERE _key = 'user1'

請看以下資料集,其中顯示煙霧和一氧化碳警報器的開啟或關閉狀態。資料欄系列為 alarmType,資料欄限定詞為 smokecarbonMonoxide。每個資料欄中的儲存格都以空白行分隔。


alarmType
_key 煙霧 carbonMonoxide
building1#section1 2023/04/01-09:10:15.000:
'off'

2023/04/01-08:41:40.000:
'on'

2020/07/03-06:25:31.000:
'off'

2020/07/03-06:02:04.000:
'on'
2023/04/01-09:22:08.000:
'off'

2023/04/01-08:53:12.000:
'on'
building1#section2 2021/03/11-07:15:04.000:
'off'

2021/03/11-07:00:25.000:
'on'

您可以使用下列查詢,找出 2023 年 4 月 1 日上午 9 點煙霧警報器開啟的 building1 區段,以及當時一氧化碳警報器的狀態。

  SELECT _key AS location, alarmType['carbonMonoxide'] AS CO_sensor
  FROM alarms(as_of => TIMESTAMP('2023-04-01T09:00:00.000Z'))
  WHERE _key LIKE 'building1%' and alarmType['smoke'] = 'on'

結果如下:

location CO_sensor
building1#section1 'on'

查詢時間序列資料

Bigtable 的常見用途是儲存時間序列資料。請看以下範例資料集,其中顯示天氣感應器的溫度和濕度讀數。資料欄系列 ID 為 metrics,資料欄限定詞則為 temperaturehumidity。資料欄中的儲存格會以空白行分隔,每個儲存格代表附有時間戳記的感應器讀數。


指標
_key 溫度 濕度
sensorA#20230105 2023/01/05-02:00:00.000:
54

2023/01/05-01:00:00.000:
56

2023/01/05-00:00:00.000:
55
2023/01/05-02:00:00.000:
0.89

2023/01/05-01:00:00.000:
0.9

2023/01/05-00:00:00.000:
0.91
sensorA#20230104 2023/01/04-23:00:00.000:
56

2023/01/04-22:00:00.000:
57
2023/01/04-23:00:00.000:
0.9

2023/01/04-22:00:00.000:
0.91

您可以使用時間篩選條件 afterbeforeafter_or_equal 擷取特定範圍的時間戳記值。以下範例使用 after

   SELECT metrics['temperature'] AS temp_versioned
   FROM
   sensorReadings(with_history => true, after => TIMESTAMP('2023-01-04T23:00:00.000Z'),
         before => TIMESTAMP('2023-01-05T01:00:00.000Z'))
   WHERE _key LIKE 'sensorA%'

查詢會傳回下列格式的資料:

temp_versioned
[{timestamp: '2023/01/05-01:00:00.000', value:56}
{timestamp: '2023/01/05-00:00:00.000', value: 55}]
[{timestamp: '2023/01/04-23:00:00.000', value:56}]

UNPACK 時間序列資料

分析時間序列資料時,通常會偏好使用表格格式的資料。Bigtable UNPACK 函式可派上用場。

UNPACK 是 Bigtable 資料表值函式 (TVF),會傳回整個輸出資料表,而非單一純量值,並出現在 FROM 子句中,就像資料表子查詢一樣。UNPACK TVF 會將每個含有時間戳記的值擴展為多個資料列 (每個時間戳記對應一個資料列),並將時間戳記移至 _timestamp 欄。

UNPACK 的輸入是子查詢,其中 with_history => true

輸出內容是展開的表格,每列都有 _timestamp 資料欄。

輸入資料欄系列 MAP<key, ARRAY<STRUCT<timestamp, value>>> 會解壓縮為 MAP<key, value>,資料欄限定詞 ARRAY<STRUCT<timestamp, value>>> 會解壓縮為 value。其他輸入資料欄類型則維持不變。必須在子查詢中選取資料欄,才能解壓縮及選取。不必選取新資料欄,系統就會解壓縮時間戳記。_timestamp

以「查詢時間序列資料」中的時間序列範例為基礎,並使用該節中的查詢做為輸入內容,您的 UNPACK 查詢格式如下:

  SELECT temp_versioned, _timestamp
  FROM
  UNPACK((
    SELECT metrics['temperature'] AS temperature_versioned
    FROM
    sensorReadings(with_history => true, after => TIMESTAMP('2023-01-04T23:00:00.000Z'),
          before => TIMESTAMP('2023-01-05T01:00:00.000Z'))
    WHERE _key LIKE 'sensorA%'
  ));

查詢會傳回下列格式的資料:

temp_versioned

_timestamp

55

1672898400

55

1672894800

56

1672891200

查詢 JSON

您可以使用 JSON 函式,針對作業工作負載,操控以 Bigtable 值形式儲存的 JSON。

舉例來說,您可以使用下列查詢,從 session 資料欄系列的最新儲存格中,擷取 JSON 元素 abc 的值,以及資料列鍵。

  SELECT _key, JSON_VALUE(session['payload'],'$.abc') AS abc FROM analytics

逸出特殊字元和保留字

Bigtable 在命名資料表和資料欄時,提供高度彈性。 因此,在 SQL 查詢中,您的資料表名稱可能需要逸出,因為特殊字元或保留字。

舉例來說,由於資料表名稱中有句號,因此下列查詢不是有效的 SQL。

  -- ERROR: Table name format not supported

  SELECT * FROM my.table WHERE _key = 'r1'

不過,只要以倒引號 (`) 字元括住項目,即可解決這個問題。

  SELECT * FROM `my.table` WHERE _key = 'r1'

如果 SQL 保留的關鍵字用做 ID,同樣可以逸出。

  SELECT * FROM `select` WHERE _key = 'r1'

搭配 Bigtable 用戶端程式庫使用 SQL

適用於 Java、Python 和 Go 的 Bigtable 用戶端程式庫支援使用 executeQuery API,透過 SQL 查詢資料。下列範例說明如何發出查詢和存取資料:

Go

如要使用這項功能,必須使用 cloud.google.com/go/bigtable 1.36.0 以上版本。如要進一步瞭解用法,請參閱 PrepareStatementBindExecuteResultRow 說明文件。

  import (
    "cloud.google.com/go/bigtable"
  )

  func query(client *bigtable.Client) {
    // Prepare once for queries that will be run multiple times, and reuse
    // the PreparedStatement for each request. Use query parameters to
    // construct PreparedStatements that can be reused.
    ps, err := client.PrepareStatement(
      "SELECT cf1['bytesCol'] AS bytesCol, CAST(cf2['stringCol'] AS STRING) AS stringCol, cf3 FROM myTable WHERE _key=@keyParam",
      map[string]SQLType{
        "keyParam": BytesSQLType{},
      }
    )
    if err != nil {
      log.Fatalf("Failed to create PreparedStatement: %v", err)
    }

    // For each request, create a BoundStatement with your query parameters set.
    bs, err := ps.Bind(map[string]any{
      "keyParam": []byte("mykey")
    })
    if err != nil {
      log.Fatalf("Failed to bind parameters: %v", err)
    }

    err = bs.Execute(ctx, func(rr ResultRow) bool {
      var byteValue []byte
      err := rr.GetByName("bytesCol", &byteValue)
      if err != nil {
        log.Fatalf("Failed to access bytesCol: %v", err)
      }
      var stringValue string
      err = rr.GetByName("stringCol", &stringValue)
      if err != nil {
        log.Fatalf("Failed to access stringCol: %v", err)
      }
      // Note that column family maps have byte valued keys. Go maps don't support
      // byte[] keys, so the map will have Base64 encoded string keys.
      var cf3 map[string][]byte
      err = rr.GetByName("cf3", &cf3)
      if err != nil {
        log.Fatalf("Failed to access cf3: %v", err)
      }
      // Do something with the data
      // ...
      return true
    })
  }

Java

如要使用這項功能,必須使用 java-bigtable 2.57.3 以上版本。如要進一步瞭解使用方式,請參閱 Javadoc 中的 prepareStatementexecuteQueryBoundStatementResultSet

  static void query(BigtableDataClient client) {
    // Prepare once for queries that will be run multiple times, and reuse
    // the PreparedStatement for each request. Use query parameters to
    // construct PreparedStatements that can be reused.
    PreparedStatement preparedStatement = client.prepareStatement(
      "SELECT cf1['bytesCol'] AS bytesCol, CAST(cf2['stringCol'] AS STRING) AS stringCol, cf3 FROM myTable WHERE _key=@keyParam",
      // For queries with parameters, set the parameter names and types here.
      Map.of("keyParam", SqlType.bytes())
    );

    // For each request, create a BoundStatement with your query parameters set.
    BoundStatement boundStatement = preparedStatement.bind()
      .setBytesParam("keyParam", ByteString.copyFromUtf8("mykey"))
      .build();

    try (ResultSet resultSet = client.executeQuery(boundStatement)) {
      while (resultSet.next()) {
        ByteString byteValue = resultSet.getBytes("bytesCol");
        String stringValue = resultSet.getString("stringCol");
        Map<ByteString, ByteString> cf3Value =
            resultSet.getMap("cf3", SqlType.mapOf(SqlType.bytes(), SqlType.bytes()));
        // Do something with the data.
      }
    }
  }

Python asyncio

如要使用這項功能,必須使用 python-bigtable 2.30.1 以上版本。

  from google.cloud.bigtable.data import BigtableDataClientAsync

  async def execute_query(project_id, instance_id, table_id):
      async with BigtableDataClientAsync(project=project_id) as client:
          query = (
            "SELECT cf1['bytesCol'] AS bytesCol, CAST(cf2['stringCol'] AS STRING) AS stringCol,"
            " cf3 FROM {table_id} WHERE _key='mykey'"
          )
          async for row in await client.execute_query(query, instance_id):
            print(row["_key"], row["bytesCol"], row["stringCol"], row["cf3"])

SELECT *用量

如果從查詢的資料表中新增或刪除資料欄系列,SELECT *查詢可能會暫時發生錯誤。因此,對於實際工作環境工作負載,建議您在查詢中指定所有資料欄系列 ID,而不是使用 SELECT *。例如,使用 SELECT cf1, cf2, cf3 而不是 SELECT *

後續步驟