BigQueryでJOINはもう古い?ARRAY/STRUCTによるネスト化でクエリを爆速・低コストにする方法【GA4の謎も解明】

BigQueryでJOINはもう古い?ARRAY/STRUCTによるネスト化でクエリを爆速・低コストにする方法【GA4の謎も解明】

はじめに:この検証のきっかけ

きっかけは、X(旧Twitter)で見かけたGoogleのエンジニアさんのある投稿でした。
「1対Nの関係にあるテーブルは、毎回JOINするのではなく、ARRAYやSTRUCTを使って1つのテーブルに『ネスト(入れ子)』しておくと、パフォーマンスが向上しコストも削減できる」という内容です。

 

 

投稿は続き物なんですが、全部読んでも、実際に何をどうしたらどうなったのか、イマイチわからなかったです。でもテーマ自体は気になったので、実際にダミーデータを作って検証してみることにしました。すると、以下の嬉しいことがありました。

  • この投稿者さん(Googleのエンジニアさん)の投稿内容を理解できた。
  • し、「なぜGA4(Google Analytics 4)のBigQueryエクスポートデータがあんなに複雑なネスト構造になっているのか」という謎がとけた。

せっかく検証して学びもあったので、検証の全貌と実務での使いどころをステップバイステップで解説してみます。

 

検証の手順

手順を以下の通りにまとめます。テーブルを作成するSQL文や、分析結果を求めるSQL文も貼り付けておきますので、興味ある皆さんは、ぜひお手元のBigQuery環境でコピペして試してみてください。

  1. 検証用データの準備:(1対Nのテーブルを作る)
  2. シナリオA(普通のJOIN)とシナリオB(ネストしたテーブルの利用)を実行する
  3. それぞれのシナリオの「実行の詳細」を比較する

 

1.検証用データの準備(1対Nのテーブルを作る)

まずは、ECサイトなどでよくある「受注ヘッダ(1側)」と「受注明細(N側)」のダミーテーブルを作成します。以下のSQLを実行するだけで準備完了です。各SQL文の1行目にあるyour_datasetのところはご自身の環境に合わせて変更してください。

-- 1側:受注ヘッダテーブルの作成
CREATE OR REPLACE TABLE `your_dataset.orders` AS
SELECT 101 AS order_id, 'C001' AS customer_id, '2026-03-16' AS order_date UNION ALL
SELECT 102, 'C002', '2026-03-16' UNION ALL
SELECT 103, 'C001', '2026-03-17';

-- N側:受注明細テーブルの作成
CREATE OR REPLACE TABLE `your_dataset.order_items` AS
SELECT 101 AS order_id, 'Apple' AS item_name, 150 AS price, 3 AS quantity UNION ALL
SELECT 101, 'Banana', 100, 1 UNION ALL
SELECT 102, 'Orange', 200, 5 UNION ALL
SELECT 103, 'Apple', 150, 2 UNION ALL
SELECT 103, 'Grape', 500, 1 UNION ALL
SELECT 103, 'Melon', 1000, 1;

以下の2つのテーブルが作成されるはずです。

ordersテーブル(いつ、誰が、注文したかはわかるが、明細が分からない)

 

order_itemsテーブル(明細。同一オーダーでも、商品が異なればレコードが分かれるので、同じorder_idが複数レコード存在している)

 

2.「通常のJOIN」と、「ネストされたテーブル」を2つのシナリオで比較

テーブルが2つできたところで、「order_id別の売上の合計」を2つのシナリオで求め、クエリの効率性を検証してみましょう。

  1. シナリオA:通常のJOINで「order_id別の売上」を求める
  2. シナリオB:あらかじめネストしたテーブルを作っておいて「order_id別の売上」を求める

では、まず、シナリオAから

シナリオA:通常のJOINで「order_id別の売上」を求める

まずは王道のやり方。2つのテーブルを order_id で結合して、売上を計算してみます。SQL中級者であれば、このSQL文は書けるかと思います。し、SQLを書けなくても「2つのテーブルを結合して、order_idごとの収益を price * quantityで計算して集計するSQL文を書いて」とお願いすると生成AIが書いてくれます。きっと。

-- 従来のJOIN方式
SELECT 
  t1.order_id,
  SUM(t2.price * t2.quantity) as total_revenue
FROM `your_dataset.orders` t1
JOIN `your_dataset.order_items` t2 ON t1.order_id = t2.order_id
GROUP BY 1;

 

このSQLを実行した結果はこうなります。うまく行ってますね。

 

上記のSQL文を実行したときの、「実行の詳細」を確認すると、以下の通りでした。

  

パフォーマンスの比較は後述しますので、次はシナリオBを実行しましょう。

 

シナリオB:あらかじめネストしたテーブルを作っておいて「order_id別の売上」を求める

それでは次に、「ネストされたテーブル」を作り、そこからデータを抽出してみましょう。せっかくフラットなテーブルがあるのに、ネストしちゃうなんて、、、と、多少心理的抵抗はありますがw、そこはぐっと抑えて以下のSQL文を実行します。

CREATE OR REPLACE TABLE `your_dataset.nested_orders`  AS
SELECT 
  t1.order_id,
  t1.customer_id,
  t1.order_date,
  ARRAY_AGG(STRUCT(t2.item_name, t2.price, t2.quantity)) as items
FROM `your_dataset.orders` t1
JOIN `your_dataset.order_items` t2 ON t1.order_id = t2.order_id
GROUP BY 1, 2, 3;

 

すると、意図通りネストされたテーブルが完成します。1レコード目のorder_idが”101″について、商品別に2行に見えますね。これがネストされた状態です。

 

 

このテーブルに対して、「order_id別の売上の合計(total_revenue)」を求めるクエリを実行しましょう。UNNESTを利用します。

SELECT 
  order_id,
  (SELECT SUM(price * quantity) FROM UNNEST(items)) as total_revenue
FROM `your_dataset.nested_orders`;

 

実行結果は以下のとおり、2つのテーブルをJOINした場合とまったく同じです。

 

 

「実行の詳細」は以下の通りです。

 


それでは、2方式の「実行の詳細」が出揃いましたので、パフォーマンスの比較をしてみましょう。

 

3.2つのシナリオのパフォーマンス比較

2つのシナリオのパフォーマンスを一覧で比較してみます。重要な指標は「消費したスロット時間」と、「行」のところにある「読み取り済レコード」の2つです。

シナリオA
テーブルをJOINして求める
シナリオB
ネストしたテーブルで求める
消費したスロット時間26ミリ秒19ミリ秒
読み取り済レコード93

 

「消費したスロット時間」は、ざっくり言うと、CPUが「データをくっつけたり、計算したりするのに費やした純粋な労働時間」の合計です。ということは、出力結果が同じであれば、値が小さいほど、効率が良いということになります。

「読み取り済レコード」は、ざっくり言うと、データベースのハードディスクから、どれだけのデータを引っ張り出してきたか、です。BigQueryは「クエリがスキャンしたデータ量(バイト数)」に対して課金されますので、値が小さいほど課金される金額が小さいということになります。

今は、1対Nの1側が3レコード、N側が6行のスモールデータですが。これが実際のビッグデータになれば、計算量と読み取り量の大幅な削減につながり、クエリの速度(「消費したスロット時間が主たる変数)は上昇し、課金コスト(「読み取りレコード」が主たる変数)は低下することになります。

 

どういう状況に向いているか?(実務での使いどころ)

「よし、じゃあ全部のテーブルをネストしよう!」と思うかもしれませんが、ちょっと待ってください。 Googleのエンジニアさんは、投稿で以下のように補足していました。

 

 

 

推奨と非推奨の違いは何でしょうか?その答えは、BigQueryの「データの書き換え(UPDATE)にかかるコスト」にあります。つまり、受註ヘッダのように、「一度できあがったらもう中身が変わらないデータ(静的なデータ)」では、ネスト化の恩恵を最大限に受けられますが、顧客マスタのように「一度できあがっても、顧客の年齢、住所、ステータスなどが代わりうるデータ」ではネスト化の恩恵を受けづらいということです。

 

つまり、今回の例では、「ネストしたテーブル」を作成するときに、「2つのテーブルのJOIN方式」では掛かっていない、広い意味でのコスト(クエリを書く手間やBigQueryの課金)が掛かっています。ネスト化の頻度が低く、個別の分析のクエリ(今回の例では、order_id別の売上の計算)の頻度が高いなら、ネスト化するときのコストは薄まります。

一方、運用時に「ネストしたテーブルを高頻度でアップデートしなくてはいけない」となると、個別の分析クエリのコストがいかに安くても、全体としてはあまり効果がない、どころか、コスト高になってしまう可能性がある。ということです。

 

もう少し具体的にすると、1の側のテーブルが「受註ヘッダ」であれば、既受注分のレコードは静的データですので、運用する時点でもなんの修正も要りません。その後に発生したデータ(新規注文)だけ、ヘッダと明細をネストした状態にして行を挿入すればよいので、運用上の追加コストは抑えられます。

例えば、上記で紹介した「ネストしたテーブル」を作成した後に、新しい注文として、order_idが104番の注文が発生したとしましょう。そのとき、「ネストしたテーブル」にorder_id、104番を加えるには、以下のクエリを実行するだけで済みます。

 

INSERT INTO `your_dataset.nested_orders` (order_id, customer_id, order_date, items)
VALUES (
  104, 
  'C003', 
  '2026-03-17',
  [
    STRUCT('Orange' AS item_name, 200 AS price, 5 AS quantity),
    STRUCT('Grape' AS item_name, 500 AS price, 2 AS quantity)
  ]
);

 

伏線回収:GA4のテーブルがネストされている最大の理由

ここまで理解すると、多くのマーケターが「やっかいだ、使いにくい」と嘆いている GA4のBigQueryデータが、なぜあんなに複雑なネスト構造(event_params 等)になっているのか、その理由を理解できます。

例えば、以下のシナリオで考えてみましょう。

 

シナリオ:GA4データから「ページ別の平均スクロール深度」を取得する(90%以外のスクロール深度も記録していた場合)
 

GA4のデータは、1行が「1つのイベント(例:page_view)」を表し、その中に「スクロール深度」や「ページタイトル」といったパラメータがネストされています。

そのため、抽出する際は以下のように UNNEST を使い、JOINなしで取得することができます。

 

SELECT 
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page_url,
  AVG((SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'percent_scrolled')) AS avg_scroll
FROM `your_dataset.events_*`
WHERE event_name = 'scroll'
GROUP BY 1;

 

一方、もし、以下のBigQueryのテーブルの1ヒット(=1レコード)が、赤枠のヘッダテーブルと、水色枠の明細テーブルに分かれていたら、明細テーブルのレコード数は膨大になり、JOINのコストは空恐ろしいことになるでしょう。

 

まとめ

今回の検証を通じて、以下のことが分かりました。

  • ARRAY/STRUCTを使ったネスト化により、JOINをなくして読み取り行数と計算コストを大幅に削減できる。
  • データ分析用にあらかじめネストしたテーブル(データマート)を作っておくことで、日々の分析クエリを速く・安くすることができる。
  • ただし、「頻繁に更新されるデータ(顧客マスタなど)」へのネストはNG。「追加されるだけのデータ(受注ヘッダ、ログなど)」に使うのが鉄則。
  • GA4のネスト構造は、ビッグデータを扱う上での大正解の姿である。

「UNNEST」という少し見慣れないSQLに抵抗を感じていた方も、その裏にある「圧倒的なメリット」を知れば、きっと強力な武器として使いこなせるはずです。ぜひ実務のデータパイプライン構築の参考にしてみてください!

 

この記事に関連した学習リソース

この記事をここまで読んでいただいた方は、もれなく、BigQuery(SQL)って、素敵なことができるな!!と思っていただいたんじゃないかと思います。

SQLについて、学習用の良い動画講座を紹介します。たくさんのレビューを頂き、5点満点中4.6点と、お陰様で評判も良いようです。よろしければどうぞ。(画像をクリックするとUdemy.comにジャンプします。)

 

書籍で学習したい場合には、以下の拙著を推奨します。こちらをどうぞ。(アマゾンのアフィリエイトリンクです。画像をクリックするとアマゾンにジャンプします)