インフラ

BigQueryのCREATE TABLE:パーティション指定とMERGEの実装指針

テーブルを1枚作るCREATE TABLEは、書こうと思えば1行で終わります。ところがそこにPARTITION BYを書き忘れると、後から直す手段はテーブルの作り直ししか残りません。分割方式は作成時にしか決められないためです。2026年8月17日時点の公式ドキュメントをもとに、CREATE TABLE構文で指定できる範囲、パーティションとクラスタリングの決め方、ビューとマテリアライズドビューの分岐、MERGEで差分を流すときの前提、スキーマ変更で作り直しが要る境界までを整理します。

まとめ:CREATE TABLEで先に決める3点と後から変えられない境界

CREATE TABLEを書く前に固定すべきは、分割方式・クラスタ列・更新方式の3点です。このうち後から戻せないのは分割方式だけ。既存の分割なしテーブルを直接パーティション分割テーブルへ変換する手段はなく、CREATE TABLE ... AS SELECTで作り直すことになります。分割に使える列は1つだけのため、PARTITION BYに何を書くかへ時間を割いてください。

クラスタ列は最大4列まで。後から変えられますが、既存データは並び替わりません。更新方式は、全件が毎回届くならCREATE OR REPLACE TABLEでの洗い替え、差分だけを当てるならMERGEという分岐です。同じ集計を何度も読む用途ではマテリアライズドビューが候補になるものの、定義SQLは作成後に変更できず、集計軸を変えるには削除と再作成が要ります。

CREATE TABLE構文の骨格:列定義とCTAS・OR REPLACEの使い分け

構文は、修飾子・テーブル名・列定義・分割指定・オプション・元クエリという並びで組み立てられています。どこまで書くかで運用の手触りが変わります。

列定義とAS SELECTの2通り:スキーマを明示する場面と推論に任せる場面

CREATE TABLEには列リストかAS query_statementのどちらか、または両方が必要です。両方を書いた場合、BigQueryはクエリ側の列名を無視し、列リストと位置で突き合わせます。名前で合わせてくれるわけではないため、列の順序がずれると型違いのデータが入ります。

列リストを省いたCREATE TABLE ... AS SELECTは、列名と型をクエリから決めます。中間テーブルや検証用の書き出しならこれで十分。外部から取り込むデータを受ける入口のテーブルでは列リストを明示してください。推論任せだとソース側の型変更が黙って伝播し、下流のクエリが壊れます。

OR REPLACEとIF NOT EXISTSの排他:再実行できるDDLの書き方

OR REPLACEIF NOT EXISTSは同時に書けないと公式ドキュメントが明記しています。似た用途に見えて、意味は逆です。前者は既存テーブルを置き換え、前回のレコードも列も残しません。後者は同名のテーブルがあれば何もしないため、初期構築のスクリプトを何度流しても既存データを消しません。

選択の基準は「再実行したときにデータが消えてよいか」の一点。日次バッチの洗い替えは前者、セットアップ用のDDLは後者です。なおOR REPLACEでは分割方式の違うテーブルへ置き換えられず、その場合はDROPしてから作り直します。

OPTIONSに書けるdescriptionとlabels:棚卸しの手間を減らす指定

OPTIONS句には説明・表示名・ラベル・有効期限を書けます。description="日次の受注明細"と書けばコンソールの一覧から用途が読め、labels=[("org_unit", "development")]の形式なら環境やチーム単位で絞り込めます。

期限系は2種類。expiration_timestampはテーブル自体の消滅時刻、partition_expiration_days=7はパーティション単位の保持期間で、後者は分割テーブルにしか設定できず、既定では期限切れになりません。ログ系のテーブルでは、この1行がストレージ料金にそのまま効きます。製品全体の位置づけはBigQueryとは何かを整理した記事で確認できます。

パーティション指定:日付・整数範囲の選択と作成後に変更できない制約

PARTITION BYに書ける式:DATE列・TRUNC系・整数範囲の選び分け

PARTITION BYに書ける式は公式ドキュメントで列挙されています。取り込み時間で日次に切る_PARTITIONDATE、DATE列をそのまま指定する形、TIMESTAMP列やDATETIME列をDATE()で日次に丸める形、粒度を選べるTIMESTAMP_TRUNC(ts_col, MONTH)DATETIME_TRUNC、DATE列を月次・年次に丸めるDATE_TRUNC(date_col, MONTH)、整数列を範囲で切るRANGE_BUCKET(id, GENERATE_ARRAY(0, 1000000, 1000))の6系統です。

DATE_TRUNCに指定できるのはMONTHとYEARのみで、TIMESTAMP系はDAYからHOURまで下げられます。時間単位の分割は、1日あたりのデータ量が大きく当日内で絞り込むクエリが多いテーブルに限ってください。取り込み時間による分割はAS query_statementと併用できず、DDLで空のテーブルを作りINSERTで入れる手順になります。

分割の粒度は1列だけ:日次と月次で変わる10,000パーティションの上限

BigQueryは複数列でのパーティショニングをサポートしていません。受注日と出荷日の両方で絞り込みたいテーブルでも、分割は片方に決め、もう一方はクラスタ列へ回すことになります。

粒度の選択は上限に直結します。1つのパーティション分割テーブルに設定できるパーティションは最大10,000個。日次なら10,000日でおよそ27年分、時間単位なら10,000時間でおよそ417日が頭打ちです。5年分のログを時間単位で持つ設計は成立しません。整数範囲の分割でも範囲数の上限は10,000個。データセットの切り方はデータセット設計を扱った記事で整理しています。

require_partition_filterで全期間スキャンを止める設定と副作用

require_partition_filter=trueを指定すると、そのテーブルへのクエリはパーティション列での絞り込みを必須にします。既定値はfalseです。分割しただけでは、WHERE句に日付条件を書き忘れたクエリが全期間を読み、課金対象バイト数がそのまま膨らみます。

副作用として、全期間を集計したい正当なクエリもエラーになります。raw層の大きなテーブルにtrueを設定し、集計済みのmart層には設定しない切り分けが扱いやすい形。設定は作成後でもALTER TABLE ... SET OPTIONSで変えられます。読み取りバイト数と料金の関係はBigQueryの料金を整理した記事で確認してください。

クラスタリング指定:最大4列の順序設計とパーティション併用のコスト差

CLUSTER BYは最大4列:並び順が絞り込み効率を決める理由

CLUSTER BYに指定できるクラスタ化列は最大4つ。型はDATE、BOOL、GEOGRAPHY、INT64、NUMERIC、BIGNUMERIC、STRING、TIMESTAMP、DATETIMEに限られます。

並び順には意味があります。CLUSTER BY tenant_id, product_idと書いた場合、tenant_idだけで絞るクエリは効きますが、product_idだけで絞るクエリはほとんど効きません。WHERE句に最も頻繁に登場する列を先頭に置いてください。STRING列には制限が1つあり、BigQueryは並べ替えに先頭1,024文字だけを見ます。前半が同じで後半だけ違うURLのような値では効果が出ません。

パーティションとクラスタリングの併用:上限を超える設計の逃がし方

PARTITION BY order_date CLUSTER BY tenant_idと書くと、日付で範囲を絞ったうえでテナント単位の並びが効き、それぞれ単独より読み取り量が減ります。この併用が生きるのは、パーティション数の上限に当たったときの逃がし先としてです。

時間単位の分割で417日しか持てないテーブルも、日次分割へ落としてクラスタ列に時刻系の列を足せば27年分まで伸びる計算。公式ドキュメントも、上限を超える場合はパーティショニングに加えて、あるいは代わりにクラスタリングを使うよう案内しています。

後付けクラスタリングの落とし穴:既存データが並び替わらない仕様

既存の非クラスタ化テーブルを変更してクラスタ化しても、既存のデータは自動では並び替わりません。対象になるのは、クラスタ列を使って保存された新しいデータだけ。数年分が入ったテーブルにCLUSTER BYを後付けしても当面は体感が変わらないのは、これが理由です。既存データも並べ直すならUPDATEで全体を書き換えるか作り直すことになり、どちらも全体スキャンが発生します。

ビューとマテリアライズドビューの分岐:増分更新の条件と再作成が要る場面

論理ビューは定義だけを保存:実行のたびにベーステーブルを読む構造

CREATE VIEWで作る論理ビューは、SQL文の定義だけを保存します。データは持ちません。参照するたびに裏側でベーステーブルへのクエリが走り、そのぶんのバイト数が課金対象になります。ストレージ料金は発生しない代わりに、読み取り料金は毎回かかる構造です。

向いているのは、列の絞り込みや結合条件の共通化のように、定義を1箇所へまとめること自体が目的の場面。重い集計を1日に何十回も参照する用途では同じ計算を毎回やり直すことになり、ここがマテリアライズドビューとの分岐点になります。

マテリアライズドビューの既定値:30分間隔の自動更新とmax_staleness

CREATE MATERIALIZED VIEWは集計結果を実体として保存し、可能な場合は前回の更新以降の変更分だけを読む増分アップデートで結果を保ちます。オプションの既定値はenable_refresh=truerefresh_interval_minutes=30。何も書かなければ30分ごとに自動で更新されます。

費用はビューへのクエリ・更新時のメンテナンス・保存バイト数のストレージの3種類で出ます。鮮度と頻度を釣り合わせる指定がmax_stalenessで、max_staleness=INTERVAL "4:0:0" HOUR TO SECONDのように書き、既定では無効。4時間前の集計で足りるダッシュボードなら、この指定で費用を抑えられます。

定義を後から直せない制約:同名置換ができず削除と再作成になる場面

マテリアライズドビューには制約が並びます。作成後にビューのSQLを更新できません。既存のマテリアライズドビューを同じ名前のマテリアライズドビューで置き換えることもできず、集計軸を1つ足すだけでも削除してから作り直す手順になります。

他にも、ビュー同士のネストや外部テーブル・スナップショットへのクエリは不可、対応言語はGoogleSQLのみ、COPYやDMLでのデータ直接操作も不可。集計軸が固まりきっていない段階は論理ビューで走り、定義が安定してから移す順番が事故を減らします。

MERGEによる冪等なUPSERT:洗い替えとの使い分けとDML課金の扱い

MERGEの3つのWHEN句:TARGET側とSOURCE側で分かれる分岐条件

MERGEはMERGE INTO target USING source ON 条件の形で書き、続けてWHEN句を並べます。WHEN MATCHEDは両方に存在する行でUPDATEかDELETEを、WHEN NOT MATCHED BY TARGETはソースにしかない行でINSERTを、WHEN NOT MATCHED BY SOURCEはターゲットにしかない行でUPDATEかDELETEを実行します。

日次の差分取り込みでは前2つだけを書く形が基本。3つ目まで書くと、ソースに含まれない既存行が消えます。全件スナップショットが毎回届くなら正しい挙動ですが、差分ファイルしか届かない場合は過去データが全滅します。ソースが全件か差分かを確認せずにWHEN NOT MATCHED BY SOURCE THEN DELETEを書かないでください。

同じ差分を2回流しても壊れない条件:一致キーの一意性という前提

MERGEを冪等な処理として扱えるのは、ON句の一致キーがターゲット側で一意である場合に限られます。同じキーを持つ行がターゲットに2件あると、更新対象が定まりません。

BigQueryの主キーはPRIMARY KEY (id) NOT ENFORCEDの形で宣言できますが、名前のとおり強制されず、宣言しただけでは重複を防げません。実務では、ソース側でQUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC) = 1のように最新1行へ絞ってからMERGEへ渡す形が扱いやすい構成。QUALIFYの書き方はQUALIFY句を扱った記事で整理しています。

洗い替えとMERGEの分岐:CREATE OR REPLACEが有利になるデータ量

判断を先に言い切ります。テーブル全体が数十GB以下で、ソース側から毎回全件が届くならCREATE OR REPLACE TABLE ... AS SELECTの洗い替えを選んでください。MERGEを書く必要はありません。処理が単純で、失敗しても再実行すれば同じ結果になります。

MERGEへ移るのは、テーブルが大きく全件の書き直しが割に合わなくなったときか、届くのが差分だけのとき。課金面では、パーティション変更数の割り当てにDMLがカウントされない点が効きます。頻繁な洗い替えでジョブ側の上限に当たっているなら、MERGEへの切り替えが解になります。

スキーマ変更の運用:ALTER TABLEで直せる範囲と作り直しが必要な境界

ADD COLUMNで足せる列と足せない列:REQUIREDとパーティション列の制限

ALTER TABLE mytable ADD COLUMN IF NOT EXISTS memo STRINGと書けば既存テーブルへ列を足せます。既存行のその列にはNULLが入り、REPEATEDモードなら空のARRAYです。IF NOT EXISTSを付けておけば同名の列があってもエラーになりません。

足せないものが4つあります。パーティション列、クラスタ列、既存のRECORDフィールド内部のネストした列、そしてREQUIREDモードの列。新しいRECORDフィールドの一部としてネストしたREQUIRED列を作ることはできますが、既存スキーマへ単独のREQUIRED列は足せません。必須列が要るなら、NULLABLEで足してアプリケーション側で担保するか、テーブルを作り直すかの二択です。

DROP COLUMNはメタデータ操作:ストレージが即座に減らない仕様

ALTER TABLE mytable DROP COLUMN IF EXISTS memoで列を削除できますが、これはメタデータのみの操作です。削除した列のストレージはすぐには解放されず、解放されるのは次にテーブルへ書き込みが発生したとき、つまりDML操作を行ったときやバックグラウンドの内部処理が動いたときになります。

公式ドキュメントは、DROP COLUMNがデータクリーンアップ操作ではないため、データが削除されるまでの保証された時間枠は存在しないと述べています。個人情報を含む列を消す場面では、この挙動は要件を満たしません。即座に回収するならSELECT * EXCEPT (memo)でテーブルを上書きしてください。

分割方式を変えるときの手順:CREATE TABLE AS SELECTで作り直す流れ

分割方式の変更にALTER TABLEは使えません。パーティショニング戦略はテーブル作成時に定義され、OR REPLACEでの置き換えも不可です。

手順は決まっています。新しい名前でCREATE TABLE new_table PARTITION BY DATE(created_at) CLUSTER BY tenant_id AS SELECT * FROM old_tableを実行し、検証してから参照先を切り替え、旧テーブルを残したまま数日運用して問題がなければ削除する流れ。1回のジョブで変更できるパーティションは最大4,000個のため、5年分の移行では期間を区切って複数回に分けます。

DDLで書き切る範囲の線引き:手作業で足りる条件とコード管理に移す境界

DDLをリポジトリに置くべき境界:分割方式が固まらないまま増える兆候

画面から作って構わないのは使い捨てのテーブルです。分析の途中経過を置く中間テーブル、1回きりの集計結果、検証用のサンプルデータ。境界は「そのテーブルを誰かが定期的に参照するか」で、参照が自分1人で今週限りなら画面操作で足ります。コンソールとbqコマンドでの初回操作はBigQueryの使い方をまとめた記事で扱っています。

コード管理へ移す判断は、集計テーブルが5枚以上あり分割方式がばらついているか、作り直しの手順を誰も再現できない状態が見えた時点。CREATE TABLE IF NOT EXISTSで書いたDDLをリポジトリに置けば、検証環境へ同じ構造を作り直せます。逆に、テーブルが3枚しかなく年に1度も変わらないなら、この整備は過剰です。

外部に任せる判断が先に来る場面:設計をやり直すコストが上回る条件

自力で進めない判断が先に来る場面もあります。すでに数TB規模のテーブルが分割なしで運用され、クエリ費用が月単位で膨らんでいるケース。この立て直しは、DDLの書き方ではなく、どのテーブルをどの粒度で切り直しどの順序で移行するかという設計の問題になります。移行中は旧テーブルと新テーブルの二重管理になり、参照元の切り替えも並行します。

判断の目安は、作り直しの対象が5テーブルを超えるか、移行期間中に止められない業務参照があるか。どちらかに当たるなら、社内で手順を固めるより外部の設計支援を挟んだほうが総コストは下がります。分割方式の見直しやMERGEへの移行を含むデータ基盤の設計は、データ分析基盤構築・MLOps構築支援で相談を受けています。体制側の判断材料はBigQuery導入の判断を扱った記事にまとめました。

よくある質問:CREATE TABLEの構文と再作成でつまずきやすい論点

テーブル定義と作り直しの場面で繰り返し出てくる質問を5つ挙げ、公式ドキュメントの記載に沿って答えます。

CREATE TABLEで作ったテーブルのパーティション設定を後から変更できますか?

変更はできません。公式ドキュメントは、パーティショニング戦略はテーブル作成時に定義されると明記しています。手段はCREATE TABLE ... AS SELECTで新しいテーブルを作り、データを移して参照先を切り替える流れ。1ジョブで変更できるパーティションは4,000個までのため、長期間のデータは期間を区切って移してください。

CREATE OR REPLACE TABLEとDROP+CREATEはどちらが安全ですか?

同じ分割方式のまま中身を入れ替えるならCREATE OR REPLACE TABLEです。1文で完結するため、削除だけ成功して作成が失敗する中間状態が生まれません。DROPしてからCREATEする手順が要るのは、分割方式やクラスタ設定を変えるときだけ。ただしDROP後に失敗するとテーブルが消えたままになるので、別名で作ってから切り替える手順を推奨します。

クラスタリング列は何列まで指定できますか?

最大4列です。5列目が必要な場合、公式ドキュメントはクラスタリングとパーティショニングの組み合わせを検討するよう案内しています。並べる順序は絞り込みの効き方を左右するため、WHERE句に最も多く現れる列を先頭に置いてください。STRING列で並べ替えに使われるのは先頭1,024文字だけです。

マテリアライズドビューのSQLを後から書き換えられますか?

書き換えられません。公式ドキュメントは、作成後にビューSQLを更新できないことと、同名のマテリアライズドビューで置き換えられないことの両方を明記しています。集計軸を変える場合は削除してから作り直す手順。参照側を止めずに入れ替えるなら、別名で新しいビューを作り、動作確認後に参照先を切り替えて旧ビューを削除します。

MERGEを毎時実行するとパーティション変更の上限に引っかかりますか?

パーティション変更数の上限には引っかかりません。DMLステートメントによる変更は1日あたりのパーティション変更数にカウントされないと公式ドキュメントが述べています。取り込み時間分割テーブルの11,000件、列パーティション分割テーブルの30,000件という上限は、読み込み・コピー・クエリの各ジョブ側にかかるもの。ただしメタデータ更新は10秒あたり50件までで、こちらにはDMLも数えられます。

関連記事

資料請求

RELATED POSTS 関連記事