【アップデート情報】 BigQueryで主キーと外部キーが正式にサポートされるようになりました

2023.07.28

Google Cloudのデータエンジニアをしています、はんざわです。
先日のアップデートで主キーと外部キー正式にGAになりました。

BigQuery release notes

とはいえ、大多数の人が想像するRDBの主キーや外部キーと挙動が違ったりする部分もあるので実際の検証も含めながら触っていきたいと思います。

できないこととできること

できないこと

  • 主キーと外部キーの制約は強制しない、つまりデフォルトでは重複もNULLも許容される。 (ユーザー側で制約に一致することを確認する必要がある)。

できること

  • 結合のパフォーマンス大幅改善

さっそくテーブルを作ってみましょう。

主キーと外部キーの登録方法

例として、以下のようなテーブルで考えてみます。

  • color_table
id color
1 red
2 yellow
3 green
  • fruit_table
number color_id name
001 1 apple
002 2 remon
003 3 melon

既存のテーブルに主キーや外部キーを登録する場合はALTER TABLE ADD [PRIMARY / FOREIGN] KEY statementを使います。

ALTER TABLE ADD PRIMARY KEY statement

ALTER TABLE ADD FOREIGN KEY statement

実際にcolor_tableidに主キーを登録し、fruit_tablenumberに主キーをcolor_idには外部キーを登録します。

ALTER TABLE test.color_table
ADD PRIMARY KEY(id) NOT ENFORCED;

ALTER TABLE test.fruit_table
ADD PRIMARY KEY(number) NOT ENFORCED,
ADD FOREIGN KEY(color_id) REFERENCES test.color_table(id) NOT ENFORCED;

もちろんCREATE TABLE statementで最初から登録することも可能です。

CREATE TABLE statement

重複データを追加してみる

以下のクエリでcolor_tableに重複データを追加してみます。

INSERT test.color_table
SELECT
  1 AS id,
  'red' AS color
  • color_table
id color
1 red
1 red
2 yellow
3 green

追加できてしまいました。
冒頭で説明した通り、主キーと外部キーの制約は強制されていませんのでユーザー側で追加できないように条件を定義する必要があります。

結合のパフォーマンス検証

Google Cloud公式のブログにも同様の検証が行われていたので併せて確認してください。

使用するテーブル

今回の検証ではBigQuery public datasetswikipediaを使用します。
wikipediaテーブルではrevision_idがユニークな値となっているのでこのカラムを結合キーに使用します。テーブルのサイズと詳細は以下の通りです。

  • テーブルのサイズ
項目 容量
行数 313,797,035
合計論理バイト数 35.69GB
合計物理バイト数 12.66GB
  • 詳細

These are unique across all revisions to all pages in a particular language and increase with time. Sorting the revisions to a page by revision_id will yield them in chronological order.

これらは、特定の言語のすべてのページのすべてのリビジョンで一意であり、時間とともに増加します。ページのリビジョンをrevision_idで並べ替えると、時系列で表示されます。(DeepL翻訳)

進め方

3種類のケースで結合した際のパフォーマンスを検証したいと思います。

case 対象テーブル1 対象テーブル2 詳細
case1 通常テーブル 通常テーブル 通常のテーブル同士
case2 通常テーブル + 主キー 通常テーブル + 主キーと外部キー キーを登録したテーブル同士
case3 クラスタ化テーブル + 主キー クラスタ化テーブル + 主キーと外部キー キーを登録し、クラスタ化したテーブル同士

使用するクエリ

以下のようなクエリでパフォーマンスの検証をします。クエリ実行時にはキャッシュの設定をオフにしています。
結合の前後で時間を測定し、その結果を保存します。この測定を各ケース毎に10回ずつ行いました。

BEGIN

  /* 使用する変数を定義 */
  DECLARE x INT64 DEFAULT 1;
  DECLARE START_TIME, END_TIME TIMESTAMP;

  /* 結果測定用のテーブル作成 */
  CREATE OR REPLACE TABLE `test.result1` (
    num INT64,
    diff FLOAT64
  );

  REPEAT

    /* 測定開始 */
    SET START_TIME = (SELECT CURRENT_TIMESTAMP());

    /* 対象のテーブルを結合する */
    CREATE OR REPLACE TEMP TABLE test1 AS (
      SELECT
        t1.*
      FROM
        test.wikipedia1 AS t1
      LEFT JOIN
        test.wikipedia2 AS t2
      ON
        t1.revision_id = t2.revision_id
    );

    /* 測定終了 */
    SET END_TIME = (SELECT CURRENT_TIMESTAMP());

    /* 検証結果を保存 */
    INSERT `test.result1`
    SELECT
      x AS num,
      TIMESTAMP_DIFF(END_TIME, START_TIME, millisecond) / 1000 as diff;

    SET x = x + 1;
    SELECT x;
    UNTIL x >= 11
  END REPEAT;

END;

結果

結果は以下の表の通りでした。

num case1 case2 case3
1 15.837 12.941 12.324
2 14.932 12.773 12.295
3 20.132 15.377 12.749
4 15.983 15.875 13.824
5 14.818 12.763 12.896
6 14.829 15.303 14.853
7 15.43 16.499 11.349
8 15.912 17.617 14.363
9 14.276 11.926 15.307
10 15.319 11.923 14.79

また、それぞれの最大値、最小値、平均値、標準偏差、スキャン量は以下の通りでした。

case 最大値 最小値 平均値 標準偏差 スキャン量
case1 20.132 14.276 15.7468 1.638 38.03GB
case2 17.617 11.923 14.2997 2.063 35.69GB
case3 15.307 11.349 13.475 1.333 35.69GB

この結果からわかること

  1. 公式記載の通り、通常のテーブルとキーを登録したテーブルではキーを登録したテーブルの方が処理の速度とスキャン量の両方で優れていた
  2. キーを登録したテーブル同士ではクラスタ化しているテーブルの方が処理速度が速かったが、スキャン量は同じだった。(要確認

また、以下は実行グラフです。上が通常のテーブル(case1)で下が主キーと外部キーを登録しているテーブル(case3)です。

  • 通常のテーブル(case1)

  • 主キーと外部キーを登録しているテーブル(case3)

このように主キーと外部キーと適切に登録することで結合部分を簡略化でき、よりクエリを最適化することが可能になります。

まとめ

今回の記事では主キーと外部キーの良さをざっくりと紹介しました。
記事で紹介した通り、制約を強制しないため既存のデータパイプラインにも適用しやすいのではないかと思っています。
使い方次第ではよりクエリの最適化を行えるため、是非試してみてください。