BigQuery ハンズオン教科書|2026-08 版
第 4 章 サンドボックスで完走可

GoogleSQL 基礎 — 分析クエリの型

JOIN・ウィンドウ関数・WITH 句、そしてパイプ構文。分析 SQL の武器を一通り持つ

この章で学ぶこと

前提: サンドボックスで完走可・無料枠内(全ハンズオンの処理量は合計でも 1.5 GB 未満 — 月間無料枠 1 TiB の 0.2% 以下。一般公開データセットのみ使用し、DML・ストリーミングは使用しません)。

クエリの基本形 — SELECT / WHERE / GROUP BY / HAVING / ORDER BY / LIMIT

GoogleSQL は Google Standard SQL の現行名称で、標準 SQL に準拠しつつ独自拡張を持つ BigQuery の SQL 方言です。素の SELECT 文の骨格は他の SQL エンジンと変わりません。ただし句を書く順序と評価される順序は一致しない点に注意してください。典型的なクエリは次の順で評価されます。

FROM  →  WHERE  →  GROUP BY と集計  →  HAVING  →  ウィンドウ関数  →  QUALIFY  →  DISTINCT  →  ORDER BY  →  LIMIT

つまり WHERE は集計より前に行を絞り込み、HAVING は集計後のグループを絞り込みます。SELECT リストに書いた別名(エイリアス)は WHERE や GROUP BY の中では基本的に参照できません(GROUP BY は例外的にエイリアスを受け付けます)が、ORDER BY からは参照できます。書く順序(FROM の前に SELECT を書く)と評価順序が食い違うのが標準構文の分かりにくさで、これは後述するパイプ構文が解決する課題のひとつです。

列を絞り込む際、必要な列を並べるだけでなくSELECT * EXCEPT と SELECT * REPLACE も便利です。EXCEPT は特定の列だけを除外し、REPLACE は列名・順序はそのままに値だけを差し替えます。

-- EXCEPT: 指定した列だけを結果から除外する
SELECT * EXCEPT (state)
FROM `bigquery-public-data.usa_names.usa_1910_current`
LIMIT 5;

-- REPLACE: 列名と順序は変えず、値だけを差し替える
SELECT * REPLACE (UPPER(name) AS name)
FROM `bigquery-public-data.usa_names.usa_1910_current`
LIMIT 5;

どちらもSELECT *の便利さを保ちながら、危険な全列取得を避けるための道具です。ただし本書の方針どおり、公開データセットに素の SELECT * を打つことは避けてください。テーブルの列数が多いほど、除外し忘れた大きな列(配列・STRUCT・長い文字列など)が処理バイト量を押し上げます。

実務の視点: SELECT * EXCEPT は「ほぼ全部の列が欲しいが 1〜2 列だけ邪魔」なケースで力を発揮します。逆に列を明示的に列挙するスタイルは、テーブルに列が追加されたときにクエリの出力が意図せず変わらないという利点があります。ダッシュボードの裏側など出力の安定性が重要なクエリでは列挙、対話的な調査では EXCEPT、と使い分けると事故が減ります。

集計関数には COUNT / SUM / AVG 以外にも覚えておくと便利な関数があります。COUNTIF(bool_expression) は条件を満たす行数を数える関数で、COUNT(CASE WHEN ... THEN 1 END) のような遠回りを避けられます。ANY_VALUE(expression [HAVING MAX|MIN having_expression]) はグループの中から任意の 1 行の値を返す関数で、GROUP BY のキーに機能的に従属する列(同じグループなら値が 1 つに決まる列)を、集計や GROUP BY に含めずに出力したいときに使います。日付・文字列関数も頻出です。日数差は DATE_DIFF(DATE '2010-07-07', DATE '2008-12-25', DAY) のように単位を指定して計算し、文字列の連結・変換には CONCAT / UPPER / LOWER / SUBSTR などをよく使います。

JOIN と WITH 句 — 複数テーブルを組み合わせる

GoogleSQL の JOIN は標準 SQL と同じ種類(INNER / LEFT [OUTER] / RIGHT [OUTER] / FULL [OUTER] / CROSS)を持ちますが、BigQuery ではテーブルが数億〜数十億行になり得るため、JOIN の選び方がそのままコストに直結します。

INNER JOIN A: id=1 A: id=2 A: id=3 B: id=1 B: id=2 B: id=4 id=3・id=4 は結果に含まれない LEFT JOIN A: id=1 A: id=2 A: id=3 B: id=1 B: id=2 NULL id=3 は B 側が NULL のまま残る
図 4-1 INNER JOIN は両テーブルでキーが一致した行だけを残し、LEFT JOIN は左テーブルの行をすべて残して一致しない右側を NULL で埋めます。

ON 句または USING 句でキーを指定しますが、両辺の型が一致していない場合、GoogleSQL は暗黙の変換をしてくれません。STRING と INT64 のような組み合わせは自動では比較できず、エラーになります(ハンズオン 4-2 で実例を確認します)。JOIN キーを決めるときは、まず両テーブルの型を確認する習慣をつけてください。

もっとも注意すべきは CROSS JOIN です。CROSS JOIN は 2 つの from_item の直積(デカルト積)を返し、片方が M 行・もう片方が N 行なら結果は M × N 行になります。カンマ区切り(FROM A, B)は暗黙の CROSS JOIN として扱われる点にも注意してください。

bikeshare_trips 2,271,152 行 × bikeshare_trips 2,271,152 行 CROSS JOIN 結果 約 5.16 兆行 (227万 × 227万) dry run は入力の スキャン量しか見ない
図 4-2 CROSS JOIN は行数の直積になります。dry run が返すのは読み取りバイト量の見積もりだけで、この行数爆発そのものは警告してくれません。
dry run は行数爆発を教えてくれない: dry run の処理バイト量見積もりは「入力テーブルから読み取るバイト数」の見積もりであり、JOIN や UNNEST で行数が増えることまでは反映しません。図 4-2 のような CROSS JOIN は dry run 上は小さく見えても、実行するとスロットを長時間占有し、クエリが失敗またはタイムアウトすることがあります。このクエリは本書では実行しません(数値は行数の掛け算による見積もりです)。

複数の集計・フィルタを段階的に積み重ねたいときは WITH 句(共通テーブル式・CTE)を使います。ネストしたサブクエリと同じ結果になりますが、名前を付けて順に読めるため可読性が上がります。

-- ネストしたサブクエリで書いた場合
SELECT station_name, avg_minutes
FROM (
  SELECT s.name AS station_name, AVG(t.duration_minutes) AS avg_minutes
  FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips` AS t
  JOIN `bigquery-public-data.austin_bikeshare.bikeshare_stations` AS s
    ON t.start_station_id = s.station_id
  GROUP BY station_name
)
WHERE avg_minutes > 20
ORDER BY avg_minutes DESC;

-- WITH 句(CTE)で書いた場合。結果は同じ
WITH station_avg AS (
  SELECT s.name AS station_name, AVG(t.duration_minutes) AS avg_minutes
  FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips` AS t
  JOIN `bigquery-public-data.austin_bikeshare.bikeshare_stations` AS s
    ON t.start_station_id = s.station_id
  GROUP BY station_name
)
SELECT station_name, avg_minutes
FROM station_avg
WHERE avg_minutes > 20
ORDER BY avg_minutes DESC;
元テーブル(JOIN) station_avg 駅ごとに平均 参照 最終 SELECT WHERE で絞る 結果 CTE は上から下へ、名前を付けて 1 回ずつ読まれる(クエリ内で複数回参照すると複数回評価され得る)
図 4-3 WITH 句は元テーブルを起点に名前付きの中間結果(CTE)を経由して最終結果へ至ります。ネストしたサブクエリと同じ実行結果を、読む順序どおりに書けるのが利点です。

WITH 句には注意点もあります。CTE の名前は、クエリの実行中だけ同名の永久テーブルを隠します。データセット名で修飾すれば(mydataset.orders のように)永久テーブルを参照できますが、CTE と同じ名前を無資格で書くと常に CTE の方が優先されます。命名がテーブル名と衝突していないか確認する癖をつけてください。

実務の視点: CTE は「同じサブクエリを何度もコピペする」事故を防ぎますが、GoogleSQL は非再帰 CTE の結果をマテリアライズ(一度計算して保持)しません。同じ CTE をクエリ内で複数箇所から参照すると、参照のたびに実行されます。WITH 句が主にもたらすのは可読性であって、計算結果の使い回しによる高速化ではない点を覚えておいてください。同じ中間結果を確実に 1 回だけ計算して使い回したい場合は、実テーブルへ書き出すことを検討してください。

ウィンドウ関数と QUALIFY 句

ウィンドウ関数は GROUP BY のように行を 1 行にまとめず、各行を保ったまま周囲の行を使った計算結果を追加します。構文は次のとおりです。

function_name(引数) OVER (
  [ PARTITION BY 分割キー ]
  [ ORDER BY 並び順 ]
  [ ROWS | RANGE BETWEEN フレーム開始 AND フレーム終了 ]
)

PARTITION BY は集計を行うグループを分け、ORDER BY はグループ内の並び順を決めます。SUM などの集計関数を分析関数として使う場合、ORDER BY を指定してフレーム句を省略すると、既定では RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(パーティションの先頭から現在行まで)が使われ、これは累積合計と同じ挙動になります。

PARTITION BY category(fruit と vegetable で別々に計算) category = fruit(ORDER BY purchases) banana: 2 apple: 8 (次の行) フレーム: 先頭 〜 現在行(apple)→ 合計 10 SUM(purchases) OVER ( PARTITION BY category ORDER BY purchases RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) 現在行が進むたびにフレームが 右へ広がる=累積合計になる
図 4-4 ウィンドウ関数は PARTITION BY で区切った範囲の中で、ORDER BY の並びに沿ってフレーム(計算対象の行の範囲)を決めます。フレーム省略時の既定は「パーティション先頭から現在行まで」で、累積合計と同じ結果になります。

よく使う関数は用途別に大きく 3 グループです。

グループ代表的な関数用途
番号付けROW_NUMBER / RANK / DENSE_RANK順位付け。RANK は同順位の次を飛ばし、DENSE_RANK は飛ばさない
ナビゲーションLAG / LEAD / FIRST_VALUE / LAST_VALUE前後の行の値を参照。前年比・前月比の計算に頻出
集計の窓化SUM / AVG / COUNT を OVER 付きで移動平均・累積合計など、行を保ったままの集計

ウィンドウ関数の結果で行を絞りたい場合、WHERE はウィンドウ関数より先に評価されるため使えません。代わりに QUALIFY 句を使います。QUALIFY は「ウィンドウ関数の結果」で行を絞るための専用の句で、SELECT リストか QUALIFY 自体にウィンドウ関数が存在することが条件です。評価順序は FROM → WHERE → GROUP BY/集計 → HAVING → ウィンドウ関数 → QUALIFY → DISTINCT → ORDER BY → LIMIT です。

-- QUALIFY を使わない場合: サブクエリで包んで WHERE する必要がある
SELECT * FROM (
  SELECT
    year, gender, name, number,
    RANK() OVER (PARTITION BY year, gender ORDER BY number DESC) AS rank
  FROM `bigquery-public-data.usa_names.usa_1910_current`
  WHERE year BETWEEN 2015 AND 2020
)
WHERE rank <= 3
ORDER BY year, gender, rank;

-- QUALIFY を使う場合: サブクエリが不要で 1 段のクエリで書ける。結果は同じ
SELECT
  year, gender, name, number,
  RANK() OVER (PARTITION BY year, gender ORDER BY number DESC) AS rank
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE year BETWEEN 2015 AND 2020
QUALIFY rank <= 3
ORDER BY year, gender, rank;

パイプ構文 — |> で書く分析クエリ

パイプ構文は GoogleSQL の別の書き方で、2026-08 時点で GA(一般提供)です(付随する TABLESAMPLE パイプ演算子のみプレビュー)。標準構文と同じ操作を、UNIX パイプのように演算子を連ねて書けます。基本形は次のとおりです。

|> 演算子名 引数

クエリは FROM だけで始められ(FROM table_name は標準構文の SELECT * FROM table_name に相当)、そこにパイプ演算子を連ねていきます。標準構文とパイプ構文は同じクエリの中で混在させることもできます(例: サブクエリだけパイプ構文で書く)。主な演算子は次のとおりです。

パイプ演算子標準構文での相当備考
WHEREWHERE / HAVING / QUALIFY3 つの役割を WHERE 1 本に統合。集計・ウィンドウ関数の後段でも WHERE を使う
AGGREGATE ... GROUP BY ...GROUP BY 句 + 集計関数独立した GROUP BY 演算子は無く、AGGREGATE に統合されている
EXTENDSELECT *, 式既存の列を残したまま計算列を追加。ウィンドウ関数も書ける
SELECTSELECT リスト列を絞り込み、既存の列は残らない
JOIN / ORDER BY / LIMIT同名の句標準構文とほぼ同じ意味・構文

特に重要なのは WHERE が HAVING と QUALIFY の役割を兼ねる点です。標準構文で「集計後にHAVING」「ウィンドウ関数の後にQUALIFY」と使い分けていたものが、パイプ構文ではすべて WHERE になります。書く順序どおりに上から下へ評価されるため、「今どの段階のデータを絞っているか」を演算子の並びがそのまま表します。

標準構文(書く順序と評価順序がずれる) ① SELECT rank, name … (書くのは先頭) ③ FROM table(評価はここから) ④ WHERE year BETWEEN … ⑤ ウィンドウ関数 RANK() を計算 ⑥ QUALIFY rank <= 3 ②は書く順序と評価順序がずれる パイプ構文(書いた順=評価順) ① FROM table ② |> WHERE year BETWEEN … ③ |> EXTEND RANK() OVER (…) ④ |> WHERE rank <= 3(QUALIFY 相当) ⑤ |> ORDER BY year, rank 上から下へ、書いた演算子の順にそのまま実行される
図 4-5 標準構文は SELECT を先頭に書きますが評価は FROM から始まるため順序がねじれます。パイプ構文は書いた演算子の並びがそのまま評価順序になります。
実務の視点: 既存の標準構文クエリを書き直す義務はありません。パイプ構文は「途中の結果をデバッグしたい」「段階的にフィルタ・集計を積み上げたい」場面で威力を発揮します。演算子を 1 つずつコメントアウトして直前までの結果を確認できるため、複雑な分析クエリを組み立てる過程との相性が特に良い書き方です。
ハンズオン 4-1|usa_names で集計・フィルタの基本を練習する

bigquery-public-data.usa_names.usa_1910_current(州別・年別の出生時の名前集計。5 列・約631万行・約186 MiB)を使い、SELECT の基本形を一通り試します。サンドボックスで完走可・無料枠内(各クエリとも 110〜180 MB 程度の見込み。テーブル全体は 5 列で 186 MiB なので、読む列数に応じて全体の 6 割〜ほぼ全量になります)。

  1. 2020 年・カリフォルニア州の人気の名前トップ 10 を、性別と件数付きで取得します。
-- 処理: 180 MB 程度(WHERE の year, state も含め実質 5 列すべてを参照)
SELECT name, gender, number
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE year = 2020 AND state = 'CA'
ORDER BY number DESC
LIMIT 10;

期待される出力: name / gender / number の 3 列が number 降順で 10 行。上位には当時人気だった男女の名前が並びます(具体的な名前と件数は年によって変わるため、実行して確認してください)。

  1. SELECT * EXCEPT と SELECT * REPLACE を試します。
-- 処理: 150 MB 程度
SELECT * EXCEPT (state)
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE year = 2020 AND gender = 'F'
ORDER BY number DESC
LIMIT 5;

-- 処理: 180 MB 程度(REPLACE は列を除外しないため実質 5 列すべてを参照)。name の値だけを大文字化して返す
SELECT * REPLACE (UPPER(name) AS name)
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE year = 2020 AND gender = 'F'
ORDER BY number DESC
LIMIT 5;

期待される出力: 1 つ目は state 列が消えた 4 列 5 行。2 つ目は列構成は同じまま name だけがすべて大文字になった 5 行です。

  1. 全期間・全州を対象に、女児の名前として合計 50 万件を超えたことがある名前を HAVING で絞り込みます。
-- 処理: 110 MB 程度(name, gender, number の 3 列。5 列中 3 列なので約 6 割)
SELECT name, SUM(number) AS total_babies
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE gender = 'F'
GROUP BY name
HAVING total_babies > 500000
ORDER BY total_babies DESC;

期待される出力: 累計 50 万件を超えた女児名だけが total_babies 降順で並びます。HAVING は SUM で作った集計後の値(エイリアス total_babies)に対する条件なので、集計前の生の行には使えないことを確認してください。

後片付け: すべて宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。クエリ結果は一時テーブルとして扱われ、自動的に期限切れになります。

ハンズオン 4-2|JOIN と WITH でバイクシェアの利用傾向を調べる

austin_bikeshare.bikeshare_trips(227万行・10 列・約252 MiB)と、駅マスタ austin_bikeshare.bikeshare_stations(101 行・16 列・15 KB とごく小さい)を JOIN します。サンドボックスで完走可・無料枠内(駅マスタは無視できるほど小さく、費用は trips 側の参照列数でほぼ決まります)。

  1. まず JOIN キーの型を確認せずに、終了駅(end_station_id)で JOIN してみます。
-- このクエリは型不一致でエラーになります(実行不要、エラー内容の確認が目的)
SELECT t.trip_id, s.name
FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips` AS t
JOIN `bigquery-public-data.austin_bikeshare.bikeshare_stations` AS s
  ON t.end_station_id = s.station_id
LIMIT 10;

期待される出力: end_station_id は STRING、駅マスタ側の station_id は INTEGER のため、比較演算子に一致するシグネチャがない旨のエラーになります(例: No matching signature for operator = for argument types: STRING, INT64。文言はバージョンにより多少異なります)。GoogleSQL は STRING と INT64 を自動変換しません。

  1. CAST で型を揃えて修正し、続けて WITH 句で「出発駅ごとの平均利用時間」を求めます。
-- 処理: 60〜90 MB 程度(start_station_id, duration_minutes の 2 列は数値型で軽い)
WITH station_trips AS (
  SELECT start_station_id, duration_minutes
  FROM `bigquery-public-data.austin_bikeshare.bikeshare_trips`
  WHERE duration_minutes BETWEEN 1 AND 180
)
SELECT
  s.name AS station_name,
  COUNT(*) AS trip_count,
  ROUND(AVG(t.duration_minutes), 1) AS avg_minutes
FROM station_trips AS t
JOIN `bigquery-public-data.austin_bikeshare.bikeshare_stations` AS s
  ON t.start_station_id = s.station_id
GROUP BY station_name
ORDER BY trip_count DESC
LIMIT 10;

期待される出力: station_name / trip_count / avg_minutes の 3 列で 10 行。利用回数が多い駅ほど上位に来ます。start_station_id(trips 側)と station_id(stations 側)はどちらも INTEGER なので、CAST なしでそのまま JOIN できることも確認してください。

後片付け: すべて宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。

ハンズオン 4-3|ウィンドウ関数で年ごとの人気ネームランキングを作る

再び usa_names.usa_1910_current を使い、RANK と QUALIFY、そして LAG で前年比を計算します。サンドボックスで完走可・無料枠内(150〜190 MB 程度の見込み。2 つ目のクエリは WHERE で name / state / gender も参照するため実質 5 列すべてが対象になります)。

  1. 2015〜2020 年について、年・性別ごとの人気トップ 3 を QUALIFY で取得します。
-- 処理: 150 MB 程度
SELECT
  year, gender, name, number,
  RANK() OVER (PARTITION BY year, gender ORDER BY number DESC) AS rank
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE year BETWEEN 2015 AND 2020
QUALIFY rank <= 3
ORDER BY year, gender, rank;

期待される出力: year / gender / name / number / rank の 5 列。年×性別の組ごとに rank が 1〜3 の行が並び、合計で 6 年 × 2 性別 × 3 位 = 36 行になります。

  1. 特定の名前(例: カリフォルニア州の女児名 Emma)について、前年からの増減を LAG で計算します。
-- 処理: 180 MB 程度(WHERE の name / state / gender も含め実質 5 列すべてを参照)
SELECT
  year,
  number,
  number - LAG(number) OVER (ORDER BY year) AS yoy_change
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE name = 'Emma' AND state = 'CA' AND gender = 'F'
ORDER BY year;

期待される出力: year / number / yoy_change の 3 列。最初の年は比較対象の前年が無いため yoy_change が NULL になり、以降は前年との差分(増減)が入ります。

後片付け: すべて宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。

ハンズオン 4-4|パイプ構文で書き直し、結果をテーブルに保存する

ハンズオン 4-1 の HAVING クエリと 4-3 の QUALIFY クエリをパイプ構文で書き直し、最後に CREATE TABLE ... AS で結果を演習用データセット handson_ch04 に保存します。サンドボックスで完走可・無料枠内(スキャン量はハンズオン 4-1・4-3 と同程度。テーブル作成自体は課金対象外)。

  1. 演習用データセットを US ロケーションに作成します。
bq mk \
  --dataset \
  --location=US \
  --description="第4章 GoogleSQL 基礎の演習用" \
  --default_table_expiration=86400 \
  --label=purpose:handson \
  PROJECT_ID:handson_ch04
  1. 4-1 の HAVING クエリをパイプ構文で書き直します。GROUP BY は独立した演算子ではなく AGGREGATE に統合され、HAVING 相当の絞り込みは WHERE で行います。
-- 処理: 110 MB 程度。結果はハンズオン 4-1 の HAVING クエリと同じ
FROM `bigquery-public-data.usa_names.usa_1910_current`
|> WHERE gender = 'F'
|> AGGREGATE SUM(number) AS total_babies GROUP BY name
|> WHERE total_babies > 500000
|> ORDER BY total_babies DESC;
  1. 4-3 の QUALIFY クエリをパイプ構文で書き直しつつ、結果を handson_ch04.top_names_2015_2020 に保存します。EXTEND でランキング列を追加し、QUALIFY 相当は WHERE で行います。EXTEND は既存の列を残したまま列を追加するだけなので、最後に SELECT で出力列を 4-3 と同じ 5 列に絞ります。
-- 処理: 150 MB 程度
CREATE TABLE `PROJECT_ID.handson_ch04.top_names_2015_2020` AS (
  FROM `bigquery-public-data.usa_names.usa_1910_current`
  |> WHERE year BETWEEN 2015 AND 2020
  |> EXTEND RANK() OVER (PARTITION BY year, gender ORDER BY number DESC) AS rank
  |> WHERE rank <= 3
  |> SELECT year, gender, name, number, rank
  |> ORDER BY year, gender, rank
);
  1. 保存されたテーブルを確認します。
-- 処理: 数十 KB 程度(保存した数十行を読むだけ)
SELECT COUNT(*) AS row_count
FROM `PROJECT_ID.handson_ch04.top_names_2015_2020`;

期待される出力: 4-3 のハンズオンと同じ 36 行が保存されていることが row_count = 36 で確認できます。

後片付け: この章で作成したリソースをすべて削除します。

bq rm -r -f -d PROJECT_ID:handson_ch04

つまずきポイント

JOIN キーの型が違うとエラーになる: STRING と INT64 のように型が異なる列同士は ON / USING で自動比較できません。CAST(列 AS 目的の型) で明示的に揃えてください。ハンズオン 4-2 の end_station_id(STRING)と station_id(INTEGER)はまさにこのパターンです。
CROSS JOIN・カンマ結合の行数爆発: CROSS JOIN や暗黙のカンマ結合(FROM A, B)は M × N 行を生成します。dry run のバイト数見積もりは小さく見えても、実行すると行数が爆発してタイムアウトすることがあります。意図せず CROSS JOIN になっていないか(JOIN 条件を書き忘れていないか)を必ず確認してください。
パイプ構文で HAVING / QUALIFY と書いてもエラーになる: パイプ構文には HAVING 演算子も QUALIFY 演算子もありません。集計後・ウィンドウ関数後の絞り込みは、どちらも WHERE 演算子で行います。標準構文の感覚で |> HAVING ... と書くと構文エラーになります。
WITH 句の名前が既存テーブル名を隠す: CTE の名前は、そのクエリの実行中だけ同名の永久テーブルより優先されます。データセット名で修飾すれば永久テーブルを参照できますが、無資格の名前を使うと常に CTE が優先されるため、意図せず古いテーブルを読んでいると誤解しないよう注意してください。

理解度チェック

Q1. CROSS JOIN がコスト面で危険なのはなぜですか?

2 つの from_item の直積(デカルト積)を返すため、片方が M 行・もう片方が N 行なら結果は M × N 行になります。dry run のバイト数見積もりは入力の読み取り量しか反映しないため、行数の爆発自体には気づけません。

Q2. QUALIFY 句を使うための条件は何ですか?

SELECT リストか QUALIFY 句自体のどちらかにウィンドウ関数が存在している必要があります。評価順序は集計・ウィンドウ関数より後、ORDER BY より前です。

Q3. パイプ構文で標準構文の GROUP BY に相当する演算子は何ですか?

AGGREGATE 演算子です。パイプ構文には独立した GROUP BY 演算子は無く、|> AGGREGATE 集計式 GROUP BY グループ化キー という形で GROUP BY 句が AGGREGATE の一部として組み込まれています。

Q4.(シナリオ)年ごと・カテゴリごとに売上上位 3 件だけを 1 段のクエリで取得したい。標準構文で最も簡潔な書き方は?
A. サブクエリで RANK を計算し外側で WHERE rank <= 3 B. SELECT リストで RANK を計算し QUALIFY rank <= 3 C. GROUP BY の後に HAVING rank <= 3 D. LIMIT 3 をカテゴリの数だけ書く

B。 QUALIFY はウィンドウ関数の結果をそのままの階層でフィルタできるため、サブクエリで包む必要がありません(A も同じ結果になりますがサブクエリが 1 段余分です)。HAVING は集計後の値を絞る句であり、行ごとのウィンドウ関数の結果には使えません(C は誤り)。LIMIT はクエリ全体の行数を切るだけで、グループごとの上位という条件は表現できません(D は誤り)。

Q5.(シナリオ)trips.end_station_id(STRING) と stations.station_id(INTEGER) を ON t.end_station_id = s.station_id で JOIN しようとしたらエラーになった。原因と対処は?
A. アクセス権限が不足している B. データセットのロケーションが一致していない C. 型が一致しないため比較できない。CAST で揃える必要がある D. テーブルが存在しない

C。 GoogleSQL は STRING と INT64 を自動変換しないため、ON t.end_station_id = CAST(s.station_id AS STRING) のように明示的に型を揃える必要があります。エラーメッセージに現れる「演算子に一致するシグネチャが無い」旨の内容が、権限やロケーション、テーブル存在の問題ではなく型の問題であることを示しています(A・B・D は誤り)。

まとめと次章へ