BigQueryで取得した数値をPHPで扱うときに気をつけること

はじめに

こんにちは!SINIS for Instagram開発チーム在籍のヒロ氏と申します。 SINIS for InstagramではInstagram Graph APIから取得したデータを一部BigQueryに保存しており、そこから中央値や平均値を算出しています。 ある日、BigQuery内でデータ移行作業を行うことになり、その一環として移行先のテーブルを新規作成しました。その際、中央値や平均値の算出に使用しているカラムについて、本来はINTEGER型であるべきところ、誤ってNUMERIC型に変更されてしまいました。その結果、移行後にフロントエンドから計算結果を参照しようとするとエラーが発生する状態となりました。本記事では、このエラー原因と、その解決方法を説明します。

どんなエラーが出たのか

まず、システム構成を簡単に説明します。 Laravelアプリケーションがクエリを生成し、BigQueryに送信します。GraphQLスキーマでは各指標の型をFloatとして定義しています。

そして今回出たエラーが下記ですが、Floatを期待していたのに違うものが渡された旨が記載されています。

実装を確認したところ、MAX や AVG を含むクエリを BigQuery に送信し、その結果を GraphQL を通してフロントエンドに渡しているだけの構成でした。 そのため、処理の流れ自体に不自然な点は見当たりませんでした。 また、問題が発生した初期段階では、この実装からエラーの原因を特定することはできませんでした。

エラー原因について

アプリケーションからBigQueryのデータを取得しにいくときにはlaravel-bigqueryというライブラリを用いています。

github.com

このライブラリはgoogle-cloud-php-bigqueryに依存しています。 そこでこのライブラリの実装を確認してみました。

https://github.com/googleapis/google-cloud-php-bigquery/blob/main/src/ValueMapper.php#L103

INTEGER型はオブジェクトまたはプリミティブを返しているのに対して、NUMERIC型の場合はオブジェクトのみを返しています。

(補足) returnInt64AsObjectは32bitのプラットフォームと互換性を保つために用意されたオプションであり、 デフォルトでfalseであるためINTEGERは通常プリミティブが返されます。 https://docs.cloud.google.com/php/docs/reference/cloud-bigquery/latest/BigQueryClient

よって原因はBigQueryのINTEGER型とNUMERIC型とでPHP側で型の扱いが異なることでした。

INTEGER型の場合

BigQuery (INT64)
  ↓ AVG(like_count) → FLOAT64型の結果
  ↓
PHP (float型)
  ↓ $res['target_count'] = 7.12 (float)
  ↓
GraphQL (Float型) ✅ 正常に受け取れる

NUMERIC型の場合

BigQuery (NUMERIC)
  ↓ AVG(like_count) → NUMERIC型の結果
  ↓
PHP (オブジェクト) ← ここが問題!
  ↓ $res['target_count'] = Google\Cloud\BigQuery\Numericオブジェクト
  ↓
GraphQL (Float型) ❌ 型エラー発生

解決法

DDLステートメントを利用して型を変更する

NUMERICは高精度・広範囲の小数が表現できる型です。 そのため高精度・広範囲の小数が求められず、かつ修正対応時間に余裕がある場合にはこの方法が推奨されます。 NUMERICからINTEGERへの変更は互換性がサポートされていないため、FLOAT64への変更が選択肢になります。 laravel-bigqueryでのFLOAT64の扱いはプリミティブなのでALTER文での型の変更のみで済みますが、 NUMERICと精度が異なるので、場合によってはプロダクトの仕様と照らし合わせて問題ないか調査の時間を要することになります。

テーブル スキーマの変更  |  BigQuery  |  Google Cloud Documentation

Data definition language (DDL) statements in GoogleSQL  |  BigQuery  |  Google Cloud Documentation

ALTER TABLE [IF EXISTS] table_name
ALTER COLUMN [IF EXISTS] column_name SET DATA TYPE column_schema

またINTEGERに戻したい場合は、互換性がないため型の変更はBigQueryのコンソール、BigQuery APIでも対応することはできません。 そのためカラムの型を変更するにはテーブルを一から作り直す必要があり、既存のテーブルを退避→新規テーブル作成という手順が必要になり、コストがかかります。

PHP側で型を変換する

サービスに影響が出ており早期解決が求められる、または高精度・広範囲の小数を利用したい場合はこの方法もあります。 Google\Cloud\BigQuery\Numericオブジェクトはgetterが用意されているので、それを使用して値をキャストすれば解決できます。

isset($res['target_count']) ? (float) $res['target_count']->get() : 0.0;

選択した解決方法

今回は以下の理由からPHP側で型を変換する手段を選択しました。

  • サービスに影響が出ており早急な対応が必要であった
  • DDLステートメントを利用してもう一度NUMERIC型からINTEGER型に戻して問題ないかの検証をする時間が残されていなかった
  • 焦りからDDLステートメントを利用して型を変更することが記されているドキュメントにたどり着くことができなかった

ただ先述した通り、時間に余裕がある場合や、NUMERIC型の精度が不要な場合はDDLステートメントでの型変更が推奨されます。

まとめ

  1. 焦っているときほど一次情報を確認し、複数の解決策を検討することが大切だと改めて痛感しました。
  2. 似たような数値型とはいえ精度が変わると挙動も変わることがあるので、改めて型の変更や意図せぬ変更がないかの確認は慎重にすべきと学びがありました。

参考リンク


テテマーチでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! herp.careers

エンジニアチームガイドはこちら! tetemarche01.notion.site