🔍

BigQueryのパーティション入門

に公開

パーティションとは?

BigQueryのテーブルを特定の条件で分割して管理する仕組み
大量のデータから必要な部分だけを効率よく処理するために使われる。

パーティションを使うメリット

  • 🔍 クエリ性能の向上
    必要なデータだけを読み取ることで、クエリの処理が速くなる。

  • 💰 コスト削減
    スキャンされるデータ量が減るため、クエリの料金を抑えられる。

  • 🧹 データ管理がしやすい
    古いデータを自動で削除したり、期間ごとに整理したりしやすくなる。

パーティションの種類

1. 時間単位列によるパーティション分割

DATE, DATETIME, TIMESTAMP 型の列を使って、日・月・年ごとにパーティションを分ける。

向いているケース

  • イベントやトランザクションが時系列で記録されているデータ
  • 日付でクエリをフィルタすることが多い場合(例:WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31'
  • データを日次、月次、年次で集計・管理したいとき
  • タイムスタンプ列がすでに存在し、それを活用したいとき

例:売上データを日付ごとに分割する

CREATE TABLE my_dataset.sales_data
PARTITION BY DATE(order_datetime) AS
SELECT * FROM source_table;

2. 取り込み時間によるパーティション分割

データが BigQuery に取り込まれたタイミングで、自動的にパーティションを分ける方式。
_PARTITIONTIME という擬似列が使われる。取り込まれた日時をもとにパーティション化されるので、元のデータに日付や時刻の列がなくても使える。

向いているケース

  • ログデータやストリーミングデータなど、取り込みタイミングで管理したい場合
  • データ投入のタイミングを基準にフィルタしたい場合

例:ログデータのテーブルを取り込み時間でパーティション化する

CREATE TABLE my_dataset.log_data
PARTITION BY _PARTITIONTIME
AS SELECT * FROM raw_logs;

3. 整数範囲によるパーティション分割(Range Partitioning)

整数型の列を使って、指定した範囲ごとにパーティションを分ける方式。
RANGE_BUCKET 関数と GENERATE_ARRAY を使って範囲を定義する。

たとえば、GENERATE_ARRAY(0, 1000, 100) を使うと、
0〜99、100〜199、200〜299…というように、100刻みでパーティションが作られる。

向いているケース

  • ユーザーID、スコア、年齢、地域コードなど、数値でグループ分けしたい場合
  • 日付や時刻ではなく、数値のレンジでデータを分割したいとき
  • クエリで数値範囲をフィルタすることが多い場合(例:WHERE user_id BETWEEN 200 AND 299

例:ユーザーIDを100刻みでパーティション化する

CREATE TABLE my_dataset.user_data
PARTITION BY RANGE_BUCKET(user_id, GENERATE_ARRAY(0, 1000, 100))
AS SELECT * FROM source_table;

パーティションの効果を比較するための検証構成

データセット構成

Terraform
resource "google_bigquery_dataset" "my_dataset" {
  dataset_id                  = "my_dataset"
  location                    = var.region
  default_table_expiration_ms = 7200000
  delete_contents_on_destroy  = true

  lifecycle {
    ignore_changes = [
      default_partition_expiration_ms
    ]
  }
}

# パーティションありテーブル
resource "google_bigquery_table" "partitioned_table" {
  dataset_id          = google_bigquery_dataset.my_dataset.dataset_id
  table_id            = "partitioned_table"
  deletion_protection = false

  schema = <<EOF
[
  {
    "name": "id",
    "type": "STRING",
    "mode": "NULLABLE"
  },
  {
    "name": "name",
    "type": "STRING",
    "mode": "NULLABLE"
  },
  {
    "name": "created_at",
    "type": "TIMESTAMP",
    "mode": "NULLABLE"
  }
]
EOF

  time_partitioning {
    type          = "DAY"
    field         = "created_at"
    expiration_ms = 2592000000
  }
}

# パーティションなしテーブル
resource "google_bigquery_table" "non_partitioned_table" {
  dataset_id          = google_bigquery_dataset.my_dataset.dataset_id
  table_id            = "non_partitioned_table"
  deletion_protection = false

  schema = <<EOF
[
  {
    "name": "id",
    "type": "STRING",
    "mode": "NULLABLE"
  },
  {
    "name": "name",
    "type": "STRING",
    "mode": "NULLABLE"
  },
  {
    "name": "created_at",
    "type": "TIMESTAMP",
    "mode": "NULLABLE"
  }
]
EOF
}

ロードするデータ

  • 900,000件 (30日分30,000件)

クエリ実行

パーティションあり

SELECT id, name, created_at
FROM `partner-nishiie-test.my_dataset.partitioned_table`
WHERE DATE(created_at) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
ORDER BY created_at DESC

パーティションなし

  SELECT id, name, created_at
  FROM `partner-nishiie-test.my_dataset.non_partitioned_table`
  WHERE DATE(created_at) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
  ORDER BY created_at DESC

検証結果

  • 処理されるバイト数が減り、結果的に課金されるバイト数にも反映されています。
    ※ 課金されたバイト数の最小が10MB
処理されたバイト数 (MB) 課金されるバイト数 (MB)
パーティションあり 7.57 10
パーティションなし 30.71 31

まとめ

  • パーティションを上手に使って、クエリを高速化、コストを削減することができる

参考

Discussion