Blog Entry  (May 30, 2017, 1:25 a.m.)

Tilo Mitra's avatar

BigQueryを利用したアプリケーション作成時に理解しておくべきジョブの概念

BigQueryをログの保存先としていると、BigQueryを使ったアプリケーションを利用・作成する機会が増えてきます。アプリケーションを作成するために必要な「ジョブ」の概念について軽くおさらいします。

BigQueryにおけるジョブとは

ジョブは簡単に言えばクエリの実行やデータのロードなどBigQueryで何らかの動作を実行させるための仕組みで、非同期で実行されます。ジョブは必ず1つのプロジェクトに紐づくので、どのプロジェクトに対して課金されるかを明らかにするのにも役立ちます。APIからジョブを実行する場合、プロジェクト内でユニークなIDを持たせて実行できます。そうすることで同じジョブを実行させてしまうことを防いだりそのIDを利用してジョブの結果を取得したりということができます。ジョブIDを指定しなかった場合はBigQueryが自動でユニークな値を割り当てます。

BigQueryのAPI

BigQueryのAPIは REST API として用意されていて、 Projects Datasets Tables Tabledata Jobs というコレクションがあります。REST API ですので get list などおなじみのメソッドでアクセスできます(コレクションの特性上用意されていないメソッドもあります)。
https://cloud.google.com/bigquery/docs/reference/rest/v2/

低レベルのAPIを使ったクエリ実行と結果の取得

Queries のようなコレクションはありませんのでクエリを実行するには Jobs を使います。 Jobs.insert のパラメータにクエリを指定し、BigQueryに対してジョブを挿入します。 するとBigQueryはジョブを実行するので Jobs.get でジョブの結果が取得できます。そのジョブが成功している場合、クエリ結果の保存先として指定したdataset・table(指定しなかった場合は無名のIDが Jobs.get の Response Body に含まれている)に対して Tabledata.list を呼び出せばクエリの結果を取得することができます。このようにBigQueryではAPIを組み合わせることで 非同期的にクエリ実行と結果の取得 ができます。

Jobs.insert のリクエストパラメータの例

{
  "configuration": {
    "query": {
      "query": "select * from [project-id-001:dataset.table] limit 5"
    }
  }
}

Jobs.get のレスポンスの例

{
 // (一部省略)
 "kind": "bigquery#job",
 "configuration": {
  "query": {
   "query": "select * from [project-id-001:dataset.table] limit 5"
   "destinationTable": {
    "projectId": "project-id-001",
    "datasetId": "[_dataset_id...]",
    "tableId": "[table_id...]"
   },
   "createDisposition": "CREATE_IF_NEEDED",
   "writeDisposition": "WRITE_TRUNCATE"
  }
 },
 "status": {
  "state": "DONE"
 },
 "user_email": "test@example.com"
}

getQueryResult

おそらくBigQueryに挿入するジョブの多くはクエリの実行でしょうから、何度も別々のAPIを呼び出すのは少し面倒です。そこでBigQueryは getQueryResults という便利なAPIも提供してくれています。このAPIにジョブIDを渡せば Tabledata.list を呼び出さなくてもクエリの結果を返してくれます。

Jobs.query によるクエリ実行と結果の取得

クエリ実行やその他の動作をBigQueryに指示するとき、結果取得にかなり長い時間かかってくることもあるのでジョブという仕組みは都合がいいです。しかし、ジョブを挿入してその結果を取得というプロセスを踏まなければならないのでやはり面倒です。

実はこれにも便利な方法があって、 Jobs.query にクエリを乗せて呼び出せばAPI一発で 同期的にクエリの実行と結果の取得 ができます。ただし、指定したタイムアウト秒数(デフォルトでは10秒)が経過した時点で結果の取得ができなくてもリクエストが終了するという特徴があります。タイムアウトになってしまうのはそのリクエストだけで、ジョブは動き続けています。タイムアウト時にレスポンスで返ってきたジョブIDを使って Jobs.getQueryResult を呼び出せば結果を取得することができます。

アプリケーションからAPIを呼び出すときの注意

アプリケーションからBigQueryのAPIを呼び出す際はこのジョブの仕組を理解していなければなりません。 Jobs.query は便利なのですがジョブの仕組みを理解していなければ予期せずユーザーにタイムアウトエラーを見せてしまうことになります。

「クエリを実行して一定時間結果が返ってこなかったらタイムアウトになる」という仕様であれば Jobs.query で良いのですが、そうでないなら一度 Jobs.insert でジョブを挿入してあとから Jobs.getQueryResult を呼び出すのが良いでしょう。もちろんこれは実装の問題なので Jobs.query を呼んで例外処理をするのでもいいしタイムアウトとして扱うのもいいと思います。

自分が実装するときだけでなく人が書いたコードをレビューする際は特に注意する必要があります。実装者がAPIの仕様を理解していない場合も考えられるで仕様と照らし合わせて指摘するべきポイントだと思います。特にAPIをラップする何らかのライブラリを使用している場合はおそらく query() のようにわかりやすいメソッドがあるので、何も考えずに実装すると Job.query を呼ぶ率が多くなることが予想できます。

おわりに

以前pandasのコードを読んだときにどういうわけか Job.query しか呼び出していなかったと記憶しています。統計用ライブラリなのでわざとそういう設計にしているのか、あるいは自分が読んだときには単純に未実装だったのかはわかりませんが、pandasとBigQuueryを利用したアプリケーション作成時に苦労した記憶があります。BigQueryには非同期で結果を取得する仕組みがあるので、そのBigQueryを利用したアプリケーションでも非同期で実行する仕組みがある方が自分は好みです。

元の記事へ