🔑

BigQuery クエリ性能改善: Primary key で値が重複しているときの確認

2023/09/29に公開

本記事は、BigQuery の主キーと外部キーにより、クエリの性能改善が得られるかどうか検証してみたの検証の一部になります。
ここでは、Primary key で値が重複しているとき、正しい結果になるのか・PK / FK の効果を受けることができるのかを確認します。

1. データセット・テーブルの準備

BigQuery public datasetswikipedia テーブルから、以下 4 つのテーブルを用意します。

  • normal1
  • normal2
  • pk1
  • fk1

normal1normal2 は通常テーブルです。
pk1 では、値が重複している id カラムを Primary key として設定し、
fk1 では、値が一意である revision_id カラムを Foreign key を設定します。(※ idrevision_id の間には何の関係もありません)

-- データセット作成
CREATE SCHEMA IF NOT EXISTS pk_fk_dataset;

-- 通常テーブル作成
CREATE OR REPLACE TABLE pk_fk_dataset.normal1
(
    title STRING NOT NULL,
    id INTEGER,
    language STRING NOT NULL,
    wp_namespace INTEGER NOT NULL,
    is_redirect BOOLEAN,
    revision_id INTEGER,
    contributor_ip STRING,
    contributor_id INTEGER,
    contributor_username STRING,
    timestamp INTEGER NOT NULL,
    is_minor BOOLEAN,
    is_bot BOOLEAN,
    reversion_id INTEGER,
    comment STRING,
    num_characters INTEGER NOT NULL,
) AS (
     SELECT
         *
     FROM
         bigquery-public-data.samples.wikipedia
);

CREATE OR REPLACE TABLE
pk_fk_dataset.normal2
CLONE pk_fk_dataset.normal1;

-- Primary key を設定したテーブルを作成
CREATE OR REPLACE TABLE pk_fk_dataset.pk1
(
    title STRING NOT NULL,
    id INTEGER,
    language STRING NOT NULL,
    wp_namespace INTEGER NOT NULL,
    is_redirect BOOLEAN,
    revision_id INTEGER,
    contributor_ip STRING,
    contributor_id INTEGER,
    contributor_username STRING,
    timestamp INTEGER NOT NULL,
    is_minor BOOLEAN,
    is_bot BOOLEAN,
    reversion_id INTEGER,
    comment STRING,
    num_characters INTEGER NOT NULL,
    PRIMARY KEY(id) NOT ENFORCED
) AS (
     SELECT
         *
     FROM
         bigquery-public-data.samples.wikipedia
);

-- Foreign key を設定したテーブルを作成
CREATE OR REPLACE TABLE pk_fk_dataset.fk1
(
    title STRING NOT NULL,
    id INTEGER,
    language STRING NOT NULL,
    wp_namespace INTEGER NOT NULL,
    is_redirect BOOLEAN,
    revision_id INTEGER REFERENCES pk_fk_dataset.pk1(id) NOT ENFORCED,
    contributor_ip STRING,
    contributor_id INTEGER,
    contributor_username STRING,
    timestamp INTEGER NOT NULL,
    is_minor BOOLEAN,
    is_bot BOOLEAN,
    reversion_id INTEGER,
    comment STRING,
    num_characters INTEGER NOT NULL,
) AS (
     SELECT
         *
     FROM
         bigquery-public-data.samples.wikipedia
);




id カラムが重複していることの確認

SELECT
    id,
    COUNT(id) AS count
FROM
    bigquery-public-data.samples.wikipedia
GROUP BY
    id
HAVING
    count > 1

revision_id カラムが一意であることの確認

SELECT
    revision_id,
    COUNT(revision_id) AS count
FROM
    bigquery-public-data.samples.wikipedia
GROUP BY
    revision_id
HAVING
    count > 1

2. 結合の検証

2.1. 内部結合 (左: PK(重複), 右: FK(一意)) で結合解除されない場合

PK / FK テーブルでの結果 (行数: 287,135,569) は、通常テーブルの結果 (行数: 287,135,569) と同じでした。

通常テーブル

SELECT
    n1.id
FROM
    pk_fk_dataset.normal1 n1
INNER JOIN
    pk_fk_dataset.normal2 n2
ON
    n1.id = n2.revision_id

PK / FK テーブル

SELECT
    pk1.id
FROM
    pk_fk_dataset.pk1
INNER JOIN
    pk_fk_dataset.fk1
ON
    pk1.id = fk1.revision_id

2.2. 内部結合 (左: FK(一意), 右: PK(重複)) で結合解除される場合

結合解除により、PK / FK テーブルでの結果 (行数: 313,797,035) が、通常テーブルの結果 (行数: 287,135,569) と異なってしまいました。

通常テーブル

SELECT
    n1.revision_id
FROM
    pk_fk_dataset.normal1 n1
INNER JOIN
    pk_fk_dataset.normal2 n2
ON
    n1.revision_id = n2.id

PK / FK テーブル

SELECT
    fk1.revision_id
FROM
    pk_fk_dataset.fk1
INNER JOIN
    pk_fk_dataset.pk1
ON
    fk1.revision_id = pk1.id

2.3. 左外部結合 (左: PK(重複), 右: FK(一意)) で結合解除されない場合

PK / FK テーブルでの結果 (行数: 287,135,569) は、通常テーブルの結果 (行数: 287,135,569) と同じでした。

通常テーブル

SELECT
    n1.id
FROM
    pk_fk_dataset.normal1 n1
LEFT JOIN
    pk_fk_dataset.normal2 n2
ON
    n1.id = n2.revision_id

PK / FK テーブル

SELECT
    pk1.id
FROM
    pk_fk_dataset.pk1
LEFT JOIN
    pk_fk_dataset.fk1
ON
    pk1.id = fk1.revision_id

2.4. 左外部結合 (左: FK(一意), 右: PK(重複)) で結合解除される場合

結合解除により、PK / FK テーブルでの結果 (行数: 313,797,035) が、通常テーブルの結果 (行数: 583,571,457) と異なってしまいました。

通常テーブル

SELECT
    n1.revision_id
FROM
    pk_fk_dataset.normal1 n1
LEFT JOIN
    pk_fk_dataset.normal2 n2
ON
    n1.revision_id = n2.id

PK / FK テーブル

SELECT
    fk1.revision_id
FROM
    pk_fk_dataset.fk1
LEFT JOIN
    pk_fk_dataset.pk1
ON
    fk1.revision_id = pk1.id

2.5. 結合解除されないときの PK / FK の効果確認 (内部結合)

normal1 (pk1) と normal2 (fk1) 両方のカラムを取得するように、2.2. 内部結合 (左: FK(一意), 右: PK(重複)) で結合解除される場合の SQL を変更することで、結合解除されないようにし、PK / FK により結合性能が改善されるか確認します。

通常テーブル

SELECT
    n1.revision_id,
    n2.id
FROM
    pk_fk_dataset.normal1 n1
INNER JOIN
    pk_fk_dataset.normal2 n2
ON
    n1.revision_id = n2.id

PK / FK テーブル

SELECT
    fk1.revision_id,
    pk1.id
FROM
    pk_fk_dataset.fk1
INNER JOIN
    pk_fk_dataset.pk1
ON
    fk1.revision_id = pk1.id

通常テーブルPK / FK テーブルそれぞれ 10 回実行した結果は以下のようになりました。

通常テーブル

# 経過時間 (s) 消費したスロット時間 (s)
1 13 1708
2 12 1510
3 12 1214
4 13 1383
5 14 1393
6 12 1483
7 11 1371
8 14 1394
9 14 1403
10 14 1434
平均 12.9 1429

PK / FK テーブル

# 経過時間 (s) 消費したスロット時間 (s)
1 13 1644
2 12 1563
3 13 1377
4 12 1601
5 10 1339
6 12 1639
7 11 1410
8 10 1221
9 13 1316
10 13 1575
平均 11.9 1469

今回の場合、PK / FK テーブルは、通常テーブルよりもクエリの処理速度が速くなりました。

2.6. 結合解除されないときの PK / FK の効果確認 (左外部結合)

normal1 (pk1) と normal2 (fk1) 両方のカラムを取得するように、2.4. 左外部結合 (左: FK(一意), 右: PK(重複)) で結合解除される場合の SQL を変更することで、結合解除されないようにし、PK / FK により結合性能が改善されるか確認します。

通常テーブル

SELECT
    n1.revision_id,
    n2.id
FROM
    pk_fk_dataset.normal1 n1
LEFT JOIN
    pk_fk_dataset.normal2 n2
ON
    n1.revision_id = n2.id

PK / FK テーブル

SELECT
    fk1.revision_id,
    pk1.id
FROM
    pk_fk_dataset.fk1
LEFT JOIN
    pk_fk_dataset.pk1
ON
    fk1.revision_id = pk1.id

通常テーブルPK / FK テーブルそれぞれ 10 回実行した結果は以下のようになりました。

通常テーブル

# 経過時間 (s) 消費したスロット時間 (s)
1 18 2840
2 24 2990
3 20 2911
4 22 3033
5 22 3556
6 19 2873
7 22 3163
8 30 3253
9 20 3110
10 21 3132
平均 21.8 3086

PK / FK テーブル

# 経過時間 (s) 消費したスロット時間 (s)
1 19 3080
2 21 2914
3 23 3909
4 21 2917
5 20 2752
6 22 2986
7 24 3120
8 20 2628
9 27 2918
10 22 3044
平均 21.9 3027

今回の場合、PK / FK テーブルは、通常テーブルよりもクエリの処理速度が遅くなりました。(0.1秒差なので、ほぼ同じ)

3. まとめ

PK で値が重複している場合でも、結合解除は起きました。しかし、結合解除が起きる結合を行なった場合、誤った結果になりました (PK / FK を設定していない通常テーブルの結果と一致しませんでした)。

Discussion