Bigtable 用の GoogleSQL を使用してアプリケーションを開発する

Bigtable 用の GoogleSQL を使用すると、アプリケーションからクエリを実行して、コンピューティング関数を Bigtable にオフロードできます。 これにより、クライアント側の処理の必要性が軽減されます。GoogleSQL は、Bigtable やその他の Google Cloud サービス向けに実装された ANSI 準拠の構造化クエリ言語です。Bigtable 用の GoogleSQL のその他の使用方法については、 Bigtable 用の GoogleSQL の概要をご覧ください。

始める前に

以下があることを確認してください。

シナリオの例

このガイドの例では、コンソールの Google Cloud 無料トライアル インスタンスからアクセスできる サンプル weather-data テーブルを使用します。テーブルには、station_id で識別されるさまざまな気象観測所の情報が保存されているとします。各観測所は、temp_f という名前の列に華氏で気温を記録します。アプリケーションで気温を計算する代わりに、SQL クエリを使用して、結果をアプリケーションに送信する前に、Bigtable でこれらの気温を摂氏に直接変換できます。気象データテーブルのクエリの詳細については、サンプルデータをクエリするをご覧ください。

ベスト プラクティス

パフォーマンスとリソース使用率を最適化するには、アプリケーションを開発する際に次のベスト プラクティスを考慮してください。

  • クライアントを再利用する: アプリケーション用に単一の BigtableDataClient を作成します。クライアントは gRPC チャネル管理を処理し、高い同時実行性を実現するように設計されています。アプリケーションの起動後にこのクライアントを 1 回作成し、すべてのクエリで再利用することをおすすめします。
  • パラメータ化クエリを使用する: PreparedStatement を使用して、クエリロジックをデータ値から分離します。これにより、Bigtable は実行プランをキャッシュに保存し、SQL インジェクションを防ぐことができます。
  • 準備済みステートメントを再利用する: SQL 文字列ごとに PreparedStatement を 1 回だけ呼び出します。複数のリクエスト間でステートメントを保存して再利用します。

Bigtable クライアントを作成する

アプリケーションと Bigtable の間の接続を処理するには、BigtableDataClient を作成します。次のサンプルは、単一のクライアント インスタンスをアプリケーション全体で共有して、接続を効率的に管理する方法を示しています。

BigtableDataSettings settings = BigtableDataSettings.newBuilder()
    .setProject(PROJECT_ID)
    .setInstance(INSTANCE_ID)
    .build();

// The client is thread-safe and should be reused.
try (BigtableDataClient dataClient = BigtableDataClient.create(settings)) {
    System.out.println("Connected to: " + INSTANCE_ID);
}

クエリを準備する

データを SQL 文字列に直接配置するのではなく、パラメータ化クエリを使用することをおすすめします。 Bigtable 用の GoogleSQL では、パラメータ化クエリは準備済みステートメントと同等です。これにより、クエリロジックをデータ値から分離できます。

次のサンプルは、@sid などのパラメータ プレースホルダを使用して GoogleSQL クエリを定義する方法を示しています。次に、SQL 文字列と各パラメータの型を指定して PreparedStatement を作成し、次回のクエリを高速化します。

Map<String, SqlType<?>> paramTypes = new HashMap<>();
paramTypes.put("sid", SqlType.string());

String sql = "SELECT " +
             "ROUND((CAST(CAST(weather['temp_f'] AS STRING) AS INT64) - 32) * 5 / 9, 2) AS temp_celsius " +
             "FROM " + TABLE_NAME + " WHERE station_id = @sid LIMIT 1";

// Create and reuse the PreparedStatement.
PreparedStatement preparedStatement = dataClient.prepareStatement(sql, paramTypes);

クエリを実行する

パラメータ値を指定してクエリを実行します。次のサンプルは、PreparedStatement を使用してパラメータに値をバインドし、BoundStatement を作成する方法を示しています。次に、クエリを実行し、結果をループ処理して、摂氏の気温である ResultSet を出力します。

BoundStatement boundStatement = preparedStatement.bind()
    .setStringParam("sid", stationId)
    .build();

try (ResultSet resultSet = dataClient.executeQuery(boundStatement)) {
    while (resultSet.next()) {
        System.out.println("Temp: " + resultSet.getDouble("temp_celsius"));
    }
}

データ構造を確認する

SQL を開発または探索する場合は、テーブルに関連付けられたメタデータを調べてデータ構造を理解することをおすすめします。Bigtable には柔軟なスキーマがあるため、SELECT * クエリを使用すると、クエリ結果がデータによって変わる可能性があります。そのため、本番環境アプリケーションで SELECT * クエリを使用することをおすすめしません。

次のサンプルでは、結果セットのメタデータ(列名や型など)を確認し、データとは別にクライアントに返送します。これにより、最初に列情報を取得し、アプリケーションでデータを処理するために必要なデータ構造を作成できます。

ResultSetMetadata metadata = resultSet.getMetadata();
System.out.print("Columns: ");
metadata.getColumnsList().forEach(col -> System.out.print(col.getName() + " "));
System.out.println();

列ファミリーを操作する

Bigtable では、データは列ファミリーに保存されます。GoogleSQL は、これらのファミリーを列修飾子 と値のマップとして知られる 名前と値のリストとして返します。列ファミリーを読み取ると、Bigtable は SqlType.Map 型を返します。履歴のあるテーブルから列ファミリーを読み取ると、Bigtable は SqlType.historicalMap() 型を返します。これは、列修飾子とセルタイムスタンプと 値の配列のマップです。SELECT column FROM my_table(with_history=>true)SqlType 型のその他の例については、 SqlType をご覧ください。

次のサンプルは、1 列ずつ取得するのではなく、列のグループ全体を一度に取得する方法を示しています。このコードでは、すべての名前と値をループ処理できるように、列ファミリーをマップに保存します。

Map<String, SqlType<?>> paramTypes = new HashMap<>();
paramTypes.put("keyPrefix", SqlType.bytes());
String sql = String.format("SELECT weather FROM %s WHERE STARTS_WITH(_key, @keyPrefix)", TABLE_NAME);
PreparedStatement preparedStatement = dataClient.prepareStatement(sql, paramTypes);
BoundStatement boundStatement = preparedStatement.bind()
   .setBytesParam("keyPrefix", ByteString.copyFromUtf8(key))
   .build();


try (ResultSet resultSet = dataClient.executeQuery(boundStatement)) {
   while (resultSet.next()) {
     SqlType.Map<ByteString, ByteString> mapType = SqlType.mapOf(SqlType.bytes(), SqlType.bytes());
     Map<ByteString, ByteString> weatherValues = resultSet.getMap("weather", mapType);
     for (Map.Entry<ByteString, ByteString> entry : weatherValues.entrySet()) {
       System.out.printf("%s = %s\n",
         entry.getKey().toStringUtf8(),
         entry.getValue().toStringUtf8());
   }
 }
}

次のステップ