Bigtable 用の GoogleSQL の概要
GoogleSQL ステートメントを使用して Bigtable データに対してクエリを実行できます。GoogleSQL は、BigQuery や Spanner などの他の Google Cloudサービスにも実装されている ANSI 準拠の構造化クエリ言語(SQL)です。
このドキュメントでは、Bigtable 用の GoogleSQL の概要について説明します。Bigtable で使用できる SQL クエリの例を示し、Bigtable テーブル スキーマとの関係について説明します。このドキュメントをお読みになる前に、Bigtable ストレージ モデルとスキーマ設計のコンセプトを理解しておく必要があります。
クエリは、 Google Cloud コンソールの Bigtable Studio で作成して実行できます。また、Java 用の Bigtable クライアント ライブラリ、Python、Go を使用してプログラムで実行することもできます。詳細については、Bigtable クライアント ライブラリで SQL を使用するをご覧ください。
SQL クエリは、NoSQL データ リクエストと同じ方法でクラスタノードによって処理されます。したがって、Bigtable データに対して実行する SQL クエリを作成する場合も、テーブル全体のスキャンや複雑なフィルタを避けるなど、同じベスト プラクティスが適用されます。詳細については、読み取りとパフォーマンスをご覧ください。
ユースケース
Bigtable 用の GoogleSQL は、低レイテンシのアプリケーション開発に最適です。また、Google Cloud コンソールで SQL クエリを実行すると、テーブルのスキーマをすばやく視覚的に把握する、特定のデータが書き込まれたことを確認する、またはデータに関して発生する可能性がある問題をデバッグすることができます。
Bigtable 用の GoogleSQL の現在のリリースでは、次のような一般的な SQL 構造(ただし、これらに限定されません)はサポートされていません。
SELECT以外のデータ操作言語(DML)ステートメント(INSERT、UPDATE、DELETEなど)CREATE、ALTER、DROPなどのデータ定義言語(DDL)ステートメント- データアクセス制御ステートメント
- サブクエリ
JOIN、UNION、CTEsのクエリ構文
サポートされている関数、演算子、データ型、クエリ構文などの詳細については、Bigtable 用の GoogleSQL リファレンス ドキュメントをご覧ください。
ビュー
Bigtable 用の GoogleSQL を使用して、次のリソースを作成できます。
- 継続的マテリアライズド ビュー - 継続的に実行される SQL クエリの事前計算された結果(集計データを含む)。増分更新でソーステーブルと同期します。
- 論理ビュー - テーブルのようにクエリできる、保存された名前付きクエリ。
これらのタイプのビューと承認済みビューの比較については、テーブルとビューをご覧ください。
主なコンセプト
このセクションでは、GoogleSQL を使用して Bigtable データをクエリする際に知っておくべき重要なコンセプトについて説明します。
SQL レスポンスの列ファミリー
Bigtable では、テーブルに 1 つ以上の列ファミリーが含まれています。列ファミリーは、列をグループ化するために使用されます。GoogleSQL で Bigtable テーブルをクエリする場合、テーブルのスキーマの構成要素は次のとおりです。
- クエリ対象のテーブルの行キーに対応する
_keyという名前の特別な列 - 対象の行の列ファミリーのデータを格納するテーブル内の Bigtable 列ファミリーごとに 1 つの列
Map データ型
Bigtable 用の GoogleSQL には、列ファミリーに対応するように特別に設計されたデータ型 MAP<key, value> が含まれています。
デフォルトでは、マップ列の各行には 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 は行キー、city と state は列修飾子です。配列内の各タイムスタンプと値のペア(STRUCT)は、対象行のそれらの列内に存在するセルを表し、タイムスタンプの降順で並べ替えられます。
/*----------+------------------------------------------------------------------+
| _key | info |
+----------+------------------------------------------------------------------+
| user_123 | {"city":{<t5>:"Brooklyn", <t0>:"New York"}, "state":{<t0>:"NY"}} |
+----------+------------------------------------------------------------------*/
スパース テーブル
Bigtable の重要な機能は、柔軟なデータモデルです。Bigtable テーブルでは、行で列が使用されていない場合、その列のデータは保存されません。ある行に 1 つの列があり、次の行に 100 個の列がある場合もあります。一方、リレーショナル データベース テーブルでは、すべての行にすべての列が含まれ、通常、NULL 値は、その列のデータがない行の列に保存されます。
ただし、GoogleSQL で Bigtable テーブルをクエリする場合、未使用の列は空のマップで表され、NULL 値として返されます。これらの NULL 値は、クエリ述語として使用できます。たとえば、WHERE family['column1'] IS NOT NULL などの述語を使用して、行で column1 が使用されている場合にのみ行を返すことができます。
バイト
文字列を指定すると、GoogleSQL はデフォルトで STRING 値から BYTES 値に暗黙的にキャストします。つまり、たとえば、バイト シーケンス b'qualifier' ではなく、文字列 'qualifier' を指定できます。
Bigtable はデフォルトですべてのデータをバイトとして扱うため、ほとんどの Bigtable 列には型情報が含まれていません。ただし、GoogleSQL では、CAST 関数を使用して読み取り時にスキーマを定義できます。キャストの詳細については、変換関数をご覧ください。
時間フィルタ
次の表に、テーブルの時間要素にアクセスする際に使用できる引数を示します。引数は、フィルタされる順序で示されています。たとえば、with_history は latest_n より先に適用されます。有効なタイムスタンプを指定する必要があります。
| 引数 | 説明 |
|---|---|
as_of |
Timestamp。指定されたタイムスタンプ以下のタイムスタンプを持つ最新の値を返します。 |
with_history |
Boolean。最新の値をスカラーとして返すか、タイムスタンプ付きの値を STRUCT として返すかを制御します。 |
after |
Timestamp。入力後のタイムスタンプを持つ値(この値を除く)。with_history => TRUE が必須です。 |
after_or_equal |
Timestamp。入力後のタイムスタンプを持つ値(この値を含む)。with_history => TRUE が必須です。 |
before |
Timestamp。入力前のタイムスタンプを持つ値(この値を含まない)。with_history => TRUE が必須です。 |
latest_n |
整数。列修飾子(マップキー)ごとに返すタイムスタンプ付きの値の数。1 以上にする必要があります。with_history => TRUE が必須です。 |
その他の例については、高度なクエリ パターンをご覧ください。
基礎的なクエリ
このセクションでは、基本的な Bigtable SQL クエリの例とそれらの仕組みについて説明します。その他のサンプルクエリについては、Bigtable 用の GoogleSQL のクエリパターンの例をご覧ください。
最新バージョンを取得する
Bigtable では各列に複数のバージョンのデータを格納できますが、Bigtable 用の GoogleSQL はデフォルトで、各行のデータの最新バージョン(最新のセル)を返します。
user1 がニューヨーク州内で 2 回、ブルックリン市内で 1 回転居したことを示している次のサンプル データセットについて考えてみましょう。この例では、address が列ファミリーであり、列修飾子は street、city、state です。列内のセルは空の行で区切られます。
| 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 フィルタを使用すると、特定の時点の行の状態を取得できます。たとえば、2022 年 1 月 10 日午後 1 時 14 分時点の user の住所を知りたい場合は、次のクエリを実行します。
SELECT address
FROM myTable(as_of => TIMESTAMP('2022-01-10T13:14:00.234Z'))
WHERE _key = 'user1'
結果には、2022 年 1 月 10 日午後 1 時 14 分時点で把握されている最後の住所が表示されます。これは、2021 年 12 月 20 日 09: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 であり、列修飾子は smoke と carbonMonoxide です。各列のセルは空の行で区切られます。
alarmType |
||
|---|---|---|
| _key | smoke | 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 であり、列修飾子は temperature と humidity です。列内のセルは空の行で区切られ、各セルはタイムスタンプ付きのセンサー測定値を表します。
metrics |
||
|---|---|---|
| _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 |
時間フィルタ after、before、または after_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 は、タイムスタンプ付きの各値を複数の行(タイムスタンプごとに 1 つ)に展開し、タイムスタンプを _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 予約済みキーワードが識別子として使用されている場合も、同様にエスケープできます。
SELECT * FROM `select` WHERE _key = 'r1'
Bigtable クライアント ライブラリで SQL を使用する
Java、Python、Go 用の Bigtable クライアント ライブラリでは、executeQuery API を使用した SQL によるデータのクエリがサポートされています。クエリを発行してデータにアクセスする方法を次に示します。
Go
この機能を使用するには、cloud.google.com/go/bigtable バージョン 1.36.0 以降を使用する必要があります。使用方法の詳細については、PrepareStatement、Bind、Execute、ResultRow のドキュメントをご覧ください。
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 の prepareStatement、executeQuery、BoundStatement、ResultSet をご覧ください。
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 * クエリで一時的なエラーが発生することがあります。そのため、本番環境のワークロードでは、SELECT * を使用するのではなく、クエリですべての列ファミリー ID を指定することをおすすめします。たとえば、SELECT * ではなく SELECT cf1, cf2, cf3 を使用します。