- SELECT の基本形(WHERE / GROUP BY / HAVING / ORDER BY / LIMIT)と、列を絞る SELECT * EXCEPT / REPLACE
- JOIN の種類と BigQuery 特有の注意点(CROSS JOIN の危険性、JOIN キーの型不一致)
- WITH 句(CTE)でクエリを段階的に組み立てる方法
- ウィンドウ関数(OVER / PARTITION BY / ORDER BY / フレーム)と QUALIFY 句によるフィルタ
- パイプ構文(
|>)の主要な演算子と、従来構文との対応関係
前提: サンドボックスで完走可・無料枠内(全ハンズオンの処理量は合計でも 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 の選び方がそのままコストに直結します。
ON 句または USING 句でキーを指定しますが、両辺の型が一致していない場合、GoogleSQL は暗黙の変換をしてくれません。STRING と INT64 のような組み合わせは自動では比較できず、エラーになります(ハンズオン 4-2 で実例を確認します)。JOIN キーを決めるときは、まず両テーブルの型を確認する習慣をつけてください。
もっとも注意すべきは CROSS JOIN です。CROSS JOIN は 2 つの from_item の直積(デカルト積)を返し、片方が M 行・もう片方が N 行なら結果は M × N 行になります。カンマ区切り(FROM A, B)は暗黙の CROSS JOIN として扱われる点にも注意してください。
複数の集計・フィルタを段階的に積み重ねたいときは 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;
WITH 句には注意点もあります。CTE の名前は、クエリの実行中だけ同名の永久テーブルを隠します。データセット名で修飾すれば(mydataset.orders のように)永久テーブルを参照できますが、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(パーティションの先頭から現在行まで)が使われ、これは累積合計と同じ挙動になります。
よく使う関数は用途別に大きく 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 に相当)、そこにパイプ演算子を連ねていきます。標準構文とパイプ構文は同じクエリの中で混在させることもできます(例: サブクエリだけパイプ構文で書く)。主な演算子は次のとおりです。
| パイプ演算子 | 標準構文での相当 | 備考 |
|---|---|---|
WHERE | WHERE / HAVING / QUALIFY | 3 つの役割を WHERE 1 本に統合。集計・ウィンドウ関数の後段でも WHERE を使う |
AGGREGATE ... GROUP BY ... | GROUP BY 句 + 集計関数 | 独立した GROUP BY 演算子は無く、AGGREGATE に統合されている |
EXTEND | SELECT *, 式 | 既存の列を残したまま計算列を追加。ウィンドウ関数も書ける |
SELECT | SELECT リスト | 列を絞り込み、既存の列は残らない |
JOIN / ORDER BY / LIMIT | 同名の句 | 標準構文とほぼ同じ意味・構文 |
特に重要なのは WHERE が HAVING と QUALIFY の役割を兼ねる点です。標準構文で「集計後にHAVING」「ウィンドウ関数の後にQUALIFY」と使い分けていたものが、パイプ構文ではすべて WHERE になります。書く順序どおりに上から下へ評価されるため、「今どの段階のデータを絞っているか」を演算子の並びがそのまま表します。
bigquery-public-data.usa_names.usa_1910_current(州別・年別の出生時の名前集計。5 列・約631万行・約186 MiB)を使い、SELECT の基本形を一通り試します。サンドボックスで完走可・無料枠内(各クエリとも 110〜180 MB 程度の見込み。テーブル全体は 5 列で 186 MiB なので、読む列数に応じて全体の 6 割〜ほぼ全量になります)。
- 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 行。上位には当時人気だった男女の名前が並びます(具体的な名前と件数は年によって変わるため、実行して確認してください)。
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 行です。
- 全期間・全州を対象に、女児の名前として合計 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 のみなので、削除すべきリソースはありません。クエリ結果は一時テーブルとして扱われ、自動的に期限切れになります。
austin_bikeshare.bikeshare_trips(227万行・10 列・約252 MiB)と、駅マスタ austin_bikeshare.bikeshare_stations(101 行・16 列・15 KB とごく小さい)を JOIN します。サンドボックスで完走可・無料枠内(駅マスタは無視できるほど小さく、費用は trips 側の参照列数でほぼ決まります)。
- まず 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 を自動変換しません。
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 のみなので、削除すべきリソースはありません。
再び usa_names.usa_1910_current を使い、RANK と QUALIFY、そして LAG で前年比を計算します。サンドボックスで完走可・無料枠内(150〜190 MB 程度の見込み。2 つ目のクエリは WHERE で name / state / gender も参照するため実質 5 列すべてが対象になります)。
- 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 行になります。
- 特定の名前(例: カリフォルニア州の女児名
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-1 の HAVING クエリと 4-3 の QUALIFY クエリをパイプ構文で書き直し、最後に CREATE TABLE ... AS で結果を演習用データセット handson_ch04 に保存します。サンドボックスで完走可・無料枠内(スキャン量はハンズオン 4-1・4-3 と同程度。テーブル作成自体は課金対象外)。
- 演習用データセットを US ロケーションに作成します。
bq mk \
--dataset \
--location=US \
--description="第4章 GoogleSQL 基礎の演習用" \
--default_table_expiration=86400 \
--label=purpose:handson \
PROJECT_ID:handson_ch04
- 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;
- 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
);
- 保存されたテーブルを確認します。
-- 処理: 数十 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
つまずきポイント
ON / USING で自動比較できません。CAST(列 AS 目的の型) で明示的に揃えてください。ハンズオン 4-2 の end_station_id(STRING)と station_id(INTEGER)はまさにこのパターンです。CROSS JOIN や暗黙のカンマ結合(FROM A, B)は M × N 行を生成します。dry run のバイト数見積もりは小さく見えても、実行すると行数が爆発してタイムアウトすることがあります。意図せず CROSS JOIN になっていないか(JOIN 条件を書き忘れていないか)を必ず確認してください。HAVING 演算子も QUALIFY 演算子もありません。集計後・ウィンドウ関数後の絞り込みは、どちらも WHERE 演算子で行います。標準構文の感覚で |> HAVING ... と書くと構文エラーになります。理解度チェック
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 は誤り)。
まとめと次章へ
- SELECT の評価順序は
FROM → WHERE → GROUP BY/集計 → HAVING → ウィンドウ関数 → QUALIFY → DISTINCT → ORDER BY → LIMIT。書く順序とは一致しない - JOIN はキーの型一致が必須(自動変換されない)。
CROSS JOINと暗黙のカンマ結合は行数が M × N に爆発し、dry run では気づけない WITH句(CTE)はネストしたサブクエリと同じ結果を、読む順序どおりに書ける。ただし同名の永久テーブルより優先される点と、複数回参照時に再評価され得る点に注意- ウィンドウ関数は行を保ったまま周囲の行を使った計算を追加する。フレーム省略時の既定は「先頭〜現在行」で累積合計と同じ。結果を絞るには
WHEREではなくQUALIFYを使う - パイプ構文(GA、
TABLESAMPLEのみプレビュー)は書いた順に評価される。GROUP BYはAGGREGATEに統合され、HAVINGとQUALIFYはどちらもWHEREに統合される - クエリ結果は
CREATE TABLE ... AS SELECT(パイプ構文でも可)で宛先テーブルに保存できる