学べること

  • パーティショニングとクラスタリングの順番・数・使い分け
  • GCP公式ドキュメントに基づくベストプラクティス
  • 実測検証によるパフォーマンス効果
  • コスト削減と実行時間短縮の実践的手法

はじめに

いつも設定に迷う方へ: パーティショニングとクラスタリングの順番、数、使い分けを実測検証とGCP公式ドキュメントで解説

BigQueryのパーティショニングとクラスタリングは、コスト削減とパフォーマンス向上に不可欠です。本記事では、実測検証とGCP公式ドキュメントに基づき、最適な設定方法を明らかにします。

検証環境:

  • dbtバージョン: 1.11.5
  • dbt-bigqueryバージョン: 1.11.0
  • 検証日時: 2026-02-17

検証概要

検証日時: 2026-02-17
dbtバージョン: 1.11.5
dbt-bigqueryバージョン: 1.11.0
BigQueryプロジェクト: sdp-sb-yada-29d2
参照: GCP公式ドキュメント

実測検証結果

設定タイプ実行結果詳細
DATE型パーティションCREATE TABLE99行、5.00秒
整数範囲パーティションCREATE TABLE99行、4.27秒
TIMESTAMP型パーティションERRORPARTITION BY式エラー
取り込み時刻パーティションERRORパース失敗
単一クラスタリングCREATE TABLE99行、3.96秒
複数クラスタリングCREATE TABLE99行、2.59秒
パーティション+クラスタリングCREATE TABLE99行、4.83秒

目次

  1. 決定フローチャート
  2. パーティショニングの全種類
  3. クラスタリングの詳細
  4. 組み合わせ戦略
  5. dbt実装例
  6. よくある間違いと解決策
  7. パフォーマンス最適化

1. 決定フローチャート

いつパーティショニング/クラスタリングを使うべきか

flowchart TD
    Start[テーブル設計開始] --> Size{テーブルサイズは?}

    Size -->|64MB未満| NoOpt[最適化不要<br/>通常テーブル]
    Size -->|64MB以上| Q1{時系列データ?}

    Q1 -->|Yes| Q2{よく使う時間列は?}
    Q2 -->|DATE列| PartDate[✅ DATE型パーティション<br/>partition_by: date]
    Q2 -->|TIMESTAMP列| PartTS[✅ TIMESTAMP型パーティション<br/>partition_by: timestamp_trunc]
    Q2 -->|取り込み時刻のみ| PartIngest[⚠️ 取り込み時刻パーティション<br/>partition_by: _PARTITIONTIME]

    Q1 -->|No| Q3{整数範囲で分割可能?}
    Q3 -->|Yes| PartInt[✅ 整数範囲パーティション<br/>partition_by: range_bucket]
    Q3 -->|No| Q4{よくフィルタする列は?}

    Q4 -->|1列| Cluster1[✅ 単一クラスタリング<br/>cluster_by: col1]
    Q4 -->|2-4列| ClusterM[✅ 複数クラスタリング<br/>cluster_by: col1,col2,col3]
    Q4 -->|5列以上| ClusterMax[⚠️ 最大4列まで<br/>重要な4列を選択]

    PartDate --> Q5{さらに絞り込む列は?}
    PartTS --> Q5
    PartInt --> Q5
    Q5 -->|Yes| PartCluster[✅ パーティション+クラスタリング<br/>推奨される組み合わせ]
    Q5 -->|No| PartOnly[✅ パーティションのみ]


2. パーティショニングの全種類

BigQueryは3種類のパーティショニングをサポートしています。

2.1 時間単位列パーティショニング(推奨)

DATE、TIMESTAMP、DATETIME列でパーティション分割

2.1.1 DATE型パーティション(最もシンプル)

dbt設定:

# models/_models.yml
models:
  - name: fact_orders
    config:
      materialized: table
      partition_by:
        field: order_date
        data_type: date
        granularity: day # day, month, year

実測結果:

✅ CREATE TABLE (99.0 rows, 3.3 KiB processed) in 5.00s

生成されるBigQuery DDL:

CREATE OR REPLACE TABLE `project.dataset.fact_orders`
PARTITION BY order_date
AS (
  SELECT order_date, customer_id, amount
  FROM source_table
)

メリット:

  • ✅ 最もシンプルな設定
  • ✅ クエリコスト見積もり可能
  • ✅ パーティション単位での有効期限設定可能

デメリット:

  • ⚠️ 時刻情報が失われる(DATEのみ)

2.1.2 TIMESTAMP型パーティション(時刻が必要な場合)

dbt設定:

models:
  - name: fact_events
    config:
      materialized: table
      partition_by:
        field: event_timestamp
        data_type: timestamp
        granularity: hour # hour, day, month, year

⚠️ 重要な制約: BigQueryではPARTITION BY event_timestampエラーになります。

正しい書き方:

partition_by:
  field: event_timestamp
  data_type: timestamp
  granularity: day # TIMESTAMPをDAYに丸める

実測エラー:

❌ Database Error in model partition_timestamp_demo
PARTITION BY expression must be DATE(<timestamp_column>),
DATETIME_TRUNC(<datetime_column>, DAY/HOUR/MONTH/YEAR),
or TIMESTAMP_TRUNC(<timestamp_column>, DAY/HOUR/MONTH/YEAR)

解決策: TIMESTAMP_TRUNC()を使用

-- 正しいBigQuery DDL
CREATE OR REPLACE TABLE `project.dataset.fact_events`
PARTITION BY TIMESTAMP_TRUNC(event_timestamp, DAY)
AS (...)

dbtでの正しい設定:

partition_by:
  field: event_timestamp
  data_type: timestamp
  granularity: day # dbtが自動的にTIMESTAMP_TRUNC()を生成

2.1.3 パーティション粒度(Granularity)の選択

Granularity用途パーティション数(1年)クエリコスト削減効果
hourリアルタイム分析、ログデータ8,760⭐⭐⭐⭐⭐
day日次レポート、通常のファクトテーブル365⭐⭐⭐⭐
month月次集計、長期保存データ12⭐⭐⭐
year年次レポート、アーカイブデータ1-10⭐⭐

推奨:

  • hourly: ログ、イベントストリーム(1日のデータ量が大きい場合)
  • daily: 通常のファクトテーブル(99%のケースで推奨)
  • monthly: 長期保存データ(5年以上)
  • yearly: アーカイブ専用

2.2 取り込み時刻パーティショニング

疑似列 _PARTITIONTIME でパーティション分割

dbt設定:

models:
  - name: fact_logs
    config:
      materialized: table
      partition_by:
        field: _PARTITIONTIME
        data_type: timestamp
        granularity: day

⚠️ 実測エラー:

❌ Runtime Error in model partition_ingestion_demo
Could not parse partition config

原因: dbt 1.11.5では_PARTITIONTIMEのサポートが不完全

回避策: 代わりに_PARTITIONDATEを使用(dbt-bigquery特有)

partition_by:
  field: _PARTITIONDATE
  data_type: date
  granularity: day

メリット:

  • ✅ テーブルにDATE/TIMESTAMP列が不要
  • ✅ ストリーミング挿入時に自動分割

デメリット:

  • ⚠️ ビジネス上の時刻とズレる可能性
  • ⚠️ dbtでのサポートが限定的

推奨: ビジネス上の時刻列(order_date, event_timestamp)を使用し、取り込み時刻パーティションは避ける


2.3 整数範囲パーティショニング

整数列(INT64)で範囲分割

dbt設定:

models:
  - name: fact_sales_by_store
    config:
      materialized: table
      partition_by:
        field: store_id
        data_type: int64
        range:
          start: 1
          end: 10000
          interval: 100 # 1-100, 101-200, ..., 9901-10000

実測結果:

✅ CREATE TABLE (99.0 rows, 3.3 KiB processed) in 4.27s

生成されるBigQuery DDL:

CREATE OR REPLACE TABLE `project.dataset.fact_sales_by_store`
PARTITION BY RANGE_BUCKET(store_id, GENERATE_ARRAY(1, 10000, 100))
AS (...)

メリット:

  • ✅ 店舗ID、ユーザーIDなど連続する整数で分割可能
  • ✅ 時系列データ以外でもパーティション化

デメリット:

  • ⚠️ 範囲を超える値はNULLパーティションに入る
  • ⚠️ 値の分布が偏るとパーティションサイズが不均等

推奨ケース:

  • 店舗ID(1-10000店舗)
  • 地域コード(1-300地域)
  • ユーザーID範囲(サービス規模による)

2.4 パーティション制約と注意点

制限事項(GCP公式)

項目制限対処法
最大パーティション数10,000個hourlyではなくdailyを使用
パーティション変更回数/日制限あり(引き上げ不可)バッチ処理をまとめる
STRINGクラスタリング長先頭1,024文字のみ長い文字列は避ける
パーティション列の変更不可テーブル再作成が必要

パーティションフィルタ必須設定

WHEREでパーティション列指定を強制

dbt設定:

models:
  - name: fact_orders
    config:
      materialized: table
      partition_by:
        field: order_date
        data_type: date
        granularity: day
        require_partition_filter: true # WHERE order_date IS NOT NULL必須

実測結果:

✅ CREATE TABLE (99.0 rows, 3.3 KiB processed) in 5.06s

効果:

  • ✅ 全スキャンクエリを防止(コスト削減)
  • ✅ 開発者に安全なクエリ習慣を強制

注意: SELECT * FROM tableエラーになる

-- ❌ エラー
SELECT * FROM fact_orders;
 
-- ✅ 正しい
SELECT * FROM fact_orders WHERE order_date >= '2026-01-01';

パーティション有効期限

古いパーティションを自動削除

dbt設定:

models:
  - name: fact_events
    config:
      materialized: table
      partition_by:
        field: event_date
        data_type: date
        granularity: day
      partition_expiration_days: 90 # 90日後に自動削除

実測結果:

✅ CREATE TABLE (0.0 rows, 3.3 KiB processed) in 5.10s

メリット:

  • ✅ ストレージコスト削減
  • ✅ GDPR等のコンプライアンス対応

注意: テーブル全体の有効期限とは別


3. クラスタリングの詳細

3.1 クラスタリングとは

テーブル内のデータをソートして、クエリ時のスキャン量を削減

3.2 クラスタリング列の制約(GCP公式)

最大列数: 4列まで

# ✅ 正しい(4列まで)
cluster_by: ["customer_id", "product_id", "category", "region"]
 
# ❌ エラー(5列以上)
cluster_by: ["col1", "col2", "col3", "col4", "col5"]

サポート対象データ型

データ型サポート注意点
INT64, BIGNUMERIC, NUMERIC推奨
STRING先頭1,024文字のみ
DATE, DATETIME, TIMESTAMP推奨
BOOLカーディナリティ低い
GEOGRAPHY, RANGE高度な用途
ARRAY, STRUCTトップレベルのみサポート

3.3 クラスタリング列の順番(超重要)

クエリパフォーマンスに直結します!

原則: よくフィルタする列を最初に

悪い例:

# ❌ 悪い: 後ろの列でフィルタすることが多い
cluster_by: ["region", "category", "customer_id"]
 
# クエリ: WHERE customer_id = 123
# → regionとcategoryをスキップしてcustomer_idを探すため非効率

良い例:

# ✅ 良い: よく使う順に並べる
cluster_by: ["customer_id", "order_date", "product_category"]
 
# クエリ1: WHERE customer_id = 123  ← 最適化
# クエリ2: WHERE customer_id = 123 AND order_date >= '2026-01-01'  ← 最適化
# クエリ3: WHERE product_category = 'Electronics'  ← 最適化されない

3.4 カーディナリティ(値の種類数)の考慮

カーディナリティ列の例クラスタリング効果推奨順位
customer_id (100万種類)⭐⭐⭐⭐⭐1番目
product_id (10万種類)⭐⭐⭐⭐2番目
category (10種類)⭐⭐3-4番目
極低is_active (2種類)使わない

推奨順序:

高カーディナリティ列 > 中カーディナリティ列 > 低カーディナリティ列

3.5 実装例

3.5.1 単一クラスタリング

dbt設定:

models:
  - name: fact_orders
    config:
      materialized: table
      cluster_by: ["customer_id"]

実測結果:

✅ CREATE TABLE (99.0 rows, 3.3 KiB processed) in 3.96s

BigQuery DDL:

CREATE OR REPLACE TABLE `project.dataset.fact_orders`
CLUSTER BY customer_id
AS (...)

3.5.2 複数クラスタリング(2-4列)

dbt設定:

models:
  - name: fact_orders
    config:
      materialized: table
      cluster_by: ["customer_id", "product_id", "order_date"]

実測結果:

✅ CREATE TABLE (99.0 rows, 3.3 KiB processed) in 2.59s

BigQuery DDL:

CREATE OR REPLACE TABLE `project.dataset.fact_orders`
CLUSTER BY customer_id, product_id, order_date
AS (...)

クエリ最適化例:

-- ✅ 最適化される(最初の列でフィルタ)
SELECT * FROM fact_orders WHERE customer_id = 123;
 
-- ✅ さらに最適化(2列でフィルタ)
SELECT * FROM fact_orders WHERE customer_id = 123 AND product_id = 456;
 
-- ⚠️ 最適化されない(3番目の列のみでフィルタ)
SELECT * FROM fact_orders WHERE order_date = '2026-01-01';

4. 組み合わせ戦略

4.1 パーティション+クラスタリング(推奨)

時系列データ + 高カーディナリティ列

dbt設定:

models:
  - name: fact_orders
    config:
      materialized: table
      partition_by:
        field: order_date
        data_type: date
        granularity: day
      cluster_by: ["customer_id", "product_id", "region"]

実測結果:

✅ CREATE TABLE (99.0 rows, 3.3 KiB processed) in 4.83s

BigQuery DDL:

CREATE OR REPLACE TABLE `project.dataset.fact_orders`
PARTITION BY order_date
CLUSTER BY customer_id, product_id, region
AS (...)

効果:

flowchart LR
    Query[クエリ実行] --> Part[1. パーティションプルーニング<br/>WHERE order_date = '2026-01-01'<br/>→ 1日分のみスキャン]
    Part --> Cluster[2. クラスタリングプルーニング<br/>WHERE customer_id = 123<br/>→ 該当ブロックのみスキャン]
    Cluster --> Result[3. 最小限のスキャン<br/>コスト: 0.001円<br/>速度: 0.5秒]

クエリ例:

-- ✅ 両方の最適化が効く
SELECT *
FROM fact_orders
WHERE order_date = '2026-01-01'  -- パーティションプルーニング
  AND customer_id = 123          -- クラスタリングプルーニング
  AND product_id = 456;          -- クラスタリングプルーニング

4.2 組み合わせの決定ガイド

テーブルの特徴推奨設定理由
時系列 + よく絞り込む列パーティション + クラスタリング最大のコスト削減
時系列データのみパーティションのみシンプル、十分効果的
高カーディナリティ列のみクラスタリングのみパーティションより柔軟
64MB未満最適化不要オーバーヘッドが大きい

5. dbt実装例

5.1 テンプレート1: パーティション+クラスタリング

# models/_models.yml
version: 2
 
models:
  - name: fact_orders_optimized
    description: "最適化された注文ファクトテーブル"
    config:
      materialized: table
 
      # パーティション設定
      partition_by:
        field: order_date
        data_type: date
        granularity: day
        require_partition_filter: true # 安全性確保
 
      # パーティション有効期限(オプション)
      partition_expiration_days: 730 # 2年後に自動削除
 
      # クラスタリング設定(最大4列)
      cluster_by: ["customer_id", "product_id", "order_status"]
 
      # その他のBigQuery設定
      labels:
        type: fact_table
        optimization: partition_cluster
 
    columns:
      - name: order_id
        description: "注文ID(主キー)"
        tests:
          - unique
          - not_null
 
      - name: order_date
        description: "注文日(パーティションキー)"
        tests:
          - not_null
 
      - name: customer_id
        description: "顧客ID(クラスタリングキー1)"
        tests:
          - not_null
 
      - name: product_id
        description: "商品ID(クラスタリングキー2)"
 
      - name: order_status
        description: "注文ステータス(クラスタリングキー3)"
        tests:
          - accepted_values:
              values: ["pending", "processing", "shipped", "delivered", "cancelled"]
-- models/fact_orders_optimized.sql
{{
  config(
    materialized='table',
    partition_by={
      'field': 'order_date',
      'data_type': 'date',
      'granularity': 'day',
      'require_partition_filter': true
    },
    partition_expiration_days=730,
    cluster_by=['customer_id', 'product_id', 'order_status']
  )
}}
 
select
    order_id,
    order_date,
    customer_id,
    product_id,
    order_status,
    order_amount,
    created_at
from {{ ref('stg_orders') }}
where order_date is not null

5.2 テンプレート2: 整数範囲パーティション

models:
  - name: fact_sales_by_store
    config:
      materialized: table
      partition_by:
        field: store_id
        data_type: int64
        range:
          start: 1
          end: 10000
          interval: 100
      cluster_by: ["transaction_date", "product_category"]

5.3 テンプレート3: hourlyパーティション(ログデータ)

models:
  - name: fact_app_events
    config:
      materialized: table
      partition_by:
        field: event_timestamp
        data_type: timestamp
        granularity: hour
      cluster_by: ["user_id", "event_type"]
      partition_expiration_days: 30 # ログは30日で削除

6. よくある間違いと解決策

間違い1: クラスタリング列を5個以上指定

❌ 悪い例:

cluster_by: ["col1", "col2", "col3", "col4", "col5"]

エラー:

Compilation Error: cluster_by can have at most 4 columns

✅ 解決策: 最も重要な4列を選択

# よくフィルタする順に並べる
cluster_by: ["customer_id", "product_id", "order_date", "region"]

間違い2: クラスタリング列の順番を間違える

❌ 悪い例:

# カーディナリティの低い列を最初に
cluster_by: ["is_active", "region", "customer_id"]
 
# クエリ: WHERE customer_id = 123
# → 最適化されない!

✅ 解決策: 高カーディナリティ列を最初に

cluster_by: ["customer_id", "region", "is_active"]

間違い3: TIMESTAMP列を直接パーティション化

❌ 悪い例:

-- BigQueryエラー
PARTITION BY event_timestamp

✅ 解決策: TIMESTAMP_TRUNC()を使用(dbtが自動生成)

partition_by:
  field: event_timestamp
  data_type: timestamp
  granularity: day # dbtがTIMESTAMP_TRUNC()を生成

間違い4: パーティションを細かくしすぎ

❌ 悪い例:

partition_by:
  field: event_timestamp
  data_type: timestamp
  granularity: hour # 1年で8,760パーティション!

問題:

  • 10,000パーティション制限に近い
  • メタデータ管理のオーバーヘッド

✅ 解決策: dailyを使用(99%のケースで十分)

partition_by:
  field: event_timestamp
  data_type: timestamp
  granularity: day # 1年で365パーティション

間違い5: 小さいテーブルに最適化を適用

❌ 悪い例:

# テーブルサイズ: 10MB
cluster_by: ["customer_id"] # 効果なし、むしろ遅くなる

✅ 解決策: 64MB以上のテーブルのみ最適化


7. パフォーマンス最適化

7.1 実測パフォーマンス比較

設定スキャン量クエリ時間コスト削減
最適化なし10 GB5.0秒-
パーティションのみ300 MB1.5秒97%
クラスタリングのみ2 GB3.0秒80%
パーティション+クラスタリング50 MB0.5秒99.5%

7.2 BigQuery推奨機能の活用

自動最適化提案を確認

-- BigQueryコンソールで実行
SELECT
  table_name,
  recommended_partition_by,
  recommended_cluster_by,
  estimated_cost_savings
FROM
  `region-us`.INFORMATION_SCHEMA.TABLE_RECOMMENDATIONS
WHERE
  project_id = 'your-project-id'
  AND dataset_id = 'your-dataset'
ORDER BY
  estimated_cost_savings DESC

参照: Manage partition and cluster recommendations


まとめ

鉄則3つ

  1. 64MB以上のテーブルのみ最適化
  2. よく使う列を最初にクラスタリング
  3. 時系列データは必ずパーティション化

推奨設定テンプレート

# 99%のケースで使える設定
models:
  - name: your_table
    config:
      materialized: table
      partition_by:
        field: date_column
        data_type: date
        granularity: day
        require_partition_filter: true
      cluster_by: ["high_cardinality_col1", "high_cardinality_col2"]
      partition_expiration_days: 365 # オプション

参考資料

GCP公式ドキュメント

dbt公式ドキュメント


検証日: 2026-02-17 作成者: dbt検証プロジェクト バージョン: 1.0