- GEOGRAPHY 型が WGS84 の測地系上の点・線・多角形をどう表すか、ST_ 関数で「最寄り検索」「エリア集計」をどう組み立てるか
- APPROX_COUNT_DISTINCT と HLL++ スケッチで、巨大な COUNT(DISTINCT) を精度と引き換えに桁違いに速くする方法
- HLL スケッチを事前集計しておき MERGE で再集計するパターンで、素の集計値だけでは不可能な再集計を可能にする方法
- BigQuery Studio ノートブックと BigQuery DataFrames(bigframes.pandas)が SQL と Python をどう橋渡しするか
前提: 全ハンズオンをサンドボックスで完走可・無料枠内(合計処理量の目安は数十 MB 程度。各ハンズオンで dry run を確認)。
地理空間分析の基礎 — GEOGRAPHY 型と ST_ 関数
BigQuery の GEOGRAPHY 型は、地球表面上の点集合(点・線・多角形、およびそれらの集合)を表します。座標系は WGS84 の準拠楕円体で、辺は平面の直線ではなく球面測地線(geodesic edge)として扱われます。GEOGRAPHY 列を持つテーブルを作ると、各行が 1 つの「空間フィーチャ」(ジオメトリ + 属性)になり、テーブル全体が空間フィーチャの集合になります。
点を作る基本関数は ST_GEOGPOINT(longitude, latitude) です。経度が先、緯度が後という引数順序に注意してください。多くの地図サービスは緯度を先に表示する慣習があるため、取り違えやすいポイントです。
| 関数 | 用途 | 戻り値 |
|---|---|---|
ST_GEOGPOINT(lng, lat) | 経度・緯度から点を作る | GEOGRAPHY |
ST_DISTANCE(g1, g2) | 2 つの GEOGRAPHY 間の最短距離 | FLOAT64(メートル) |
ST_DWITHIN(g1, g2, d) | 距離 d メートル以内にあるか判定 | BOOL |
ST_INTERSECTS(g1, g2) | 少なくとも 1 点が重なるか判定 | BOOL |
ST_CONTAINS(g1, g2) | g1 が g2 を内包するか判定(境界線上は含まない) | BOOL |
ST_AREA(g) | 多角形の面積 | FLOAT64(平方メートル) |
WKT(Well-Known Text)や GeoJSON との相互変換には ST_GEOGFROMTEXT / ST_ASTEXT、ST_GEOGFROMGEOJSON / ST_ASGEOJSON を使います。
SELECT
ST_ASTEXT(ST_GEOGPOINT(-73.9855, 40.7580)) AS wkt,
ST_ASGEOJSON(ST_GEOGPOINT(-73.9855, 40.7580)) AS geojson;
-- wkt: POINT(-73.9855 40.758)
-- geojson: {"type":"Point","coordinates":[-73.9855,40.758]}
ST_CONTAINS(多角形への内包判定)、「半径 5 km 圏内」のような近さを判定したいなら ST_DWITHIN(距離ベース)を選びます。行政境界のすぐ外側にある地点(埋立地・河川敷など)を ST_CONTAINS だけで判定すると取りこぼしが出るため、「最寄りの行政区画」のような曖昧な要件では両者を組み合わせる設計が実務では珍しくありません。NYC Citi Bike のステーション一覧(約 1,800 件の小テーブル。件数は変動します)から、タイムズスクエア付近の地点に最も近い 5 件を ST_DISTANCE で検索します。サンドボックスで完走可・無料枠内(処理量は数百 KB 程度、実測は dry run で確認)。
- タイムズスクエア(経度 -73.9855、緯度 40.7580)に近い順にステーションを検索します。
-- 処理: 数十 KB 程度(実測は dry run で確認)
SELECT
name,
ST_DISTANCE(
ST_GEOGPOINT(longitude, latitude),
ST_GEOGPOINT(-73.9855, 40.7580)
) AS distance_m
FROM `bigquery-public-data.new_york.citibike_stations`
ORDER BY distance_m
LIMIT 5;
期待される出力: distance_m が小さい順に 5 件のステーション名が並びます(先頭は数十〜数百メートル程度)。ORDER BY に ST_DISTANCE の計算結果を直接使うことで、「近い順」を素直に SQL で表現できます。
後片付け: 宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。
Citi Bike ステーションが NY 州のどの郡(county)に属するかを ST_CONTAINS で判定し、郡ごとの件数を集計します。サンドボックスで完走可・無料枠内(geo_us_boundaries.counties は全米で 3,000 件強、州で絞ってもさらに小さい)。
- NY 州(state_fips_code = '36')の郡ポリゴンとステーションの位置を突き合わせます。
-- 処理: 数 MB 程度(county_geom はポリゴンなので points より大きい。dry run で確認)
SELECT
c.county_name,
COUNT(*) AS station_count
FROM `bigquery-public-data.new_york.citibike_stations` AS s
JOIN `bigquery-public-data.geo_us_boundaries.counties` AS c
ON ST_CONTAINS(c.county_geom, ST_GEOGPOINT(s.longitude, s.latitude))
WHERE c.state_fips_code = '36'
GROUP BY c.county_name
ORDER BY station_count DESC;
期待される出力: New York(マンハッタン)・Kings(ブルックリン)・Queens(クイーンズ)といった郡名と、それぞれのステーション数が並びます。最寄り検索が「距離でランキング」だったのに対し、エリア集計は「多角形への内包判定で行を紐付ける」という別の仕組みであることを確認してください。
後片付け: 宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。
近似集計と HLL++ スケッチ — 精度とコストのトレードオフ
APPROX_COUNT_DISTINCT(expression) は COUNT(DISTINCT expression) の近似値を返します。統計的な推定値であり厳密な値と一致するとは限りませんが、大量の入力に対しては COUNT(DISTINCT) より高速に処理できます。他にも近似分位数を求める APPROX_QUANTILES、頻度上位を求める APPROX_TOP_COUNT / APPROX_TOP_SUM があります。
裏側で使われているのが HLL++(HyperLogLog++)というカーディナリティ推定アルゴリズムです。BigQuery では HLL_COUNT.INIT でスケッチ(BYTES 型の圧縮表現)を作り、HLL_COUNT.MERGE / HLL_COUNT.MERGE_PARTIAL でスケッチ同士を合成し、HLL_COUNT.EXTRACT で単一スケッチからカーディナリティを取り出します。MERGE は集約関数で最終的な件数(INT64)を返すのに対し、MERGE_PARTIAL は合成後の新しいスケッチ(BYTES)を返す点が異なります。
| 関数 | 入力 | 戻り値 | 用途 |
|---|---|---|---|
HLL_COUNT.INIT(x[, precision]) | 任意の値の列 | BYTES(スケッチ) | スケッチを新規作成 |
HLL_COUNT.MERGE(sketch) | スケッチの列 | INT64 | 合成して最終件数を取得 |
HLL_COUNT.MERGE_PARTIAL(sketch) | スケッチの列 | BYTES(スケッチ) | 合成して新しいスケッチを作成 |
HLL_COUNT.EXTRACT(sketch) | 単一のスケッチ | INT64 | 1 つのスケッチから件数を取得 |
精度(precision)は 10〜24 の範囲で指定でき、既定値は 15 です。精度を上げるほどストレージ消費と誤差のトレードオフが変わります。例えば精度 10 はスケッチ 1 個あたり約 1 KiB で 95% 信頼区間の誤差が ±6.50% ですが、既定の精度 15 では約 32 KiB で ±1.15% まで縮まります。
COUNT(DISTINCT) で計算し直すのではなく、日次スケッチを 1 回作って保存しておけば、任意の期間のユニークユーザー数を HLL_COUNT.MERGE だけで求められます。ダッシュボードのように同じ粒度の集計が繰り返し参照される場面で効果が大きい手法です。Citi Bike の乗車ログから使用された自転車の台数(bikeid の distinct 数)を、正確な COUNT(DISTINCT) と近似の APPROX_COUNT_DISTINCT の両方で求めて誤差を確認します。サンドボックスで完走可・無料枠内(bikeid 列のみを読むため処理量は数十 MB 程度、実測は dry run で確認)。
- 同じデータに対して両方の関数を実行し、値と誤差率を比較します。
-- 処理: 数十 MB 程度(bikeid 列のみをスキャン。実測は dry run で確認)
SELECT
COUNT(DISTINCT bikeid) AS exact_count,
APPROX_COUNT_DISTINCT(bikeid) AS approx_count,
ROUND(
ABS(APPROX_COUNT_DISTINCT(bikeid) - COUNT(DISTINCT bikeid))
/ COUNT(DISTINCT bikeid) * 100, 3
) AS error_pct
FROM `bigquery-public-data.new_york.citibike_trips`;
期待される出力: exact_count と approx_count はごく近い値になり、error_pct は既定精度(15)の理論誤差 ±1.15%(95% CI)に収まる範囲であることが多いです。実行のたびに乱数的な誤差が乗るため、毎回まったく同じ error_pct にはなりません。
後片付け: 宛先テーブルを指定しない SELECT のみなので、削除すべきリソースはありません。
乗車開始日ごとの HLL スケッチをテーブルに保存し、後から任意の期間のユニーク bikeid 数を HLL_COUNT.MERGE だけで取り出します。CREATE TABLE は DDL なのでサンドボックスで完走可・無料枠内(処理量は数十 MB 程度)。
- 演習用データセットを作成します(ロケーションは US)。
bq mk --location=US --dataset PROJECT_ID:handson_ch15
- 日ごとの HLL スケッチを事前集計してテーブルに保存します。
-- 処理: 数十 MB 程度(starttime と bikeid のみをスキャン)
CREATE TABLE `PROJECT_ID.handson_ch15.daily_bike_sketches` AS
SELECT
DATE(starttime) AS ride_date,
HLL_COUNT.INIT(bikeid) AS bike_sketch
FROM `bigquery-public-data.new_york.citibike_trips`
GROUP BY ride_date;
- 2015 年 1 月分だけを
HLL_COUNT.MERGEで合成し、期間内のユニーク bikeid 数を求めます。生ログを読み直さず、日次スケッチだけをスキャンする点に注目してください。
-- 処理: 1 MB 未満(daily_bike_sketches の該当行だけをスキャン)
SELECT HLL_COUNT.MERGE(bike_sketch) AS unique_bikes_jan2015
FROM `PROJECT_ID.handson_ch15.daily_bike_sketches`
WHERE ride_date BETWEEN '2015-01-01' AND '2015-01-31';
期待される出力: 1 月に使用された自転車の推定台数(1 つの INT64 値)。日をまたいだ期間を変えるたびに生ログへ戻る必要がなく、WHERE 句で対象の ride_date を絞ってスケッチを合成するだけで再集計できることを確認してください。
後片付け:
bq rm -r -f -d PROJECT_ID:handson_ch15
BigQuery Studio ノートブックと BigQuery DataFrames — Python × BigQuery
BigQuery Studio のノートブックは Colab Enterprise 上で動作し、SQL セルと Python セルを 1 つのインターフェースで組み合わせられます。ノートブックの実行には Compute Engine の VM が割り当てられる「ランタイム」が必要で、複数のノートブックが同じランタイムを共有できますが、各ランタイムは 1 人のユーザー専用です。ノートブックのスロット使用量は Cloud Billing レポートで goog-bq-feature-type ラベルの値 BQ_STUDIO_NOTEBOOK を使って追跡できます。
ノートブックの中心的な価値がBigQuery DataFrames(bigframes.pandas)です。pandas 互換の API を追加設定なしで使え、実際の計算はローカルではなく BigQuery のクエリエンジン側で実行されます。
import bigframes.pandas as bpd
bpd.options.bigquery.project = "your_gcp_project_id"
bpd.options.bigquery.ordering_mode = "partial"
df = bpd.read_gbq("bigquery-public-data.ml_datasets.penguins")
df.peek()
# pandas と同じ書き方だが、計算は BigQuery 側で実行される
average_body_mass = df["body_mass_g"].mean()
ordering_mode は "partial"(効率重視、行の順序は明示的にソートするまで保証されない)と "strict"(既定値、pandas 互換性重視)を選べます。df.peek() は結果を効率的にプレビューするための専用メソッドです。
ノートブックや DataFrames を使わない、より薄い選択肢がクライアントライブラリです。Python では google-cloud-bigquery パッケージを使い、クエリを投げて結果を受け取るだけの最小構成を書けます。
from google.cloud import bigquery
client = bigquery.Client()
results = client.query_and_wait(
"SELECT name, num_bikes_available "
"FROM `bigquery-public-data.new_york.citibike_stations` "
"WHERE num_bikes_available > 30"
)
for row in results:
print(f"{row.name}: {row.num_bikes_available}")
DataFrames が「pandas ライクな変換をクエリに翻訳する」レイヤーであるのに対し、クライアントライブラリは「SQL 文字列を投げて結果を受け取る」だけの薄いレイヤーです。既存のアプリケーションに組み込むならクライアントライブラリ、データ探索や特徴量エンジニアリングを対話的に進めるならノートブック + DataFrames、という使い分けになります。
つまずきポイント
ST_GEOGPOINT(longitude, latitude) は経度が先です。緯度を先に渡すと、経度・緯度の範囲チェック(緯度は -90〜90)に引っかかってエラーになるか、まったく違う地点を指す点になります。地図サービスの「緯度, 経度」表記の慣習と逆であることを覚えておいてください。ST_DWITHIN の use_spheroid パラメータは現時点では FALSE のみサポートで、球面上の距離として計算されます。HLL_COUNT.MERGE は最終件数(INT64)を返しますが、HLL_COUNT.MERGE_PARTIAL は合成後の新しいスケッチ(BYTES)を返します。さらに集計を続けたい中間結果としてテーブルに保存するなら MERGE_PARTIAL、その場で件数が欲しいなら MERGE です。異なる precision で作られたスケッチを合成すると、精度は低い方に自動的に切り下げられます。理解度チェック
Q1. ST_GEOGPOINT の引数の順序はどちらが正しいですか?
ST_GEOGPOINT(longitude, latitude)、つまり経度が先で緯度が後です。多くの地図サービスの「緯度, 経度」表記とは逆なので、取り違えに注意が必要です。
Q2. ST_CONTAINS と ST_DWITHIN の使い分けの基準は何ですか?
「どの行政区画に属するか」のような多角形への内包を厳密に判定したいなら ST_CONTAINS、「半径◯ km 圏内」のような距離ベースの近さを判定したいなら ST_DWITHIN を使います。境界のすぐ外側にある地点を漏らしたくない場合は、両者を組み合わせる設計を検討します。
Q3. HLL_COUNT.MERGE と HLL_COUNT.MERGE_PARTIAL の戻り値の違いは何ですか?
HLL_COUNT.MERGE はスケッチを合成した最終的なカーディナリティ(INT64)を返します。HLL_COUNT.MERGE_PARTIAL はスケッチを合成した新しいスケッチ(BYTES)を返し、さらに別のスケッチと合成したり、後で HLL_COUNT.EXTRACT や HLL_COUNT.MERGE にかけたりできます。
Q4.(シナリオ)日次のアクティブユーザー数を毎日集計しており、任意の期間(週次・月次・四半期)のユニークユーザー数をダッシュボードで高速に表示したい。生ログは巨大で、期間ごとに毎回 COUNT(DISTINCT) をかけると重い。最適な設計は?
A. 生ログに対して毎回 COUNT(DISTINCT) を実行するクエリをそのままダッシュボードに使う B. 日次で HLL_COUNT.INIT のスケッチを事前集計してテーブルに保存し、期間ごとに HLL_COUNT.MERGE で合成する C. APPROX_TOP_COUNT で上位ユーザーだけを集計する D. 生ログをすべて BigQuery DataFrames にロードしてローカルで集計する
B。 日次スケッチを事前集計しておけば、期間を変えるたびに生ログへ戻らずスケッチだけを合成でき、ダッシュボードの応答が速くなります。A は生ログを毎回スキャンし直すため遅く高コストです。C は上位頻度を求める関数でユニークユーザー数の推定には向きません。D は DataFrames が pandas ライクな API でクエリを組み立てる仕組みであり、ローカルに全データを持ってくる設計ではありません。
Q5.(シナリオ)BigQuery サンドボックスだけでノートブックを開いて BigQuery DataFrames を試そうとしたところ、想定通りに進められなかった。最も可能性が高い原因は?
A. GEOGRAPHY 型がサンドボックスで使えないため B. ノートブックのランタイムが Compute Engine の VM であり、課金が有効なプロジェクトが前提のため C. APPROX_COUNT_DISTINCT がサンドボックスで使えないため D. HLL スケッチの precision が範囲外のため
B。 ノートブックはColab Enterpriseのランタイム(Compute Engine VM)を必要とし、DataFrames のクイックスタートでも課金の有効化が前提条件として明示されています。A・C は地理空間・近似集計ともにこの章のハンズオンで実際にサンドボックスで完走しており誤りです。D は精度の指定範囲(10〜24)とは無関係な原因です。
まとめと次章へ
- GEOGRAPHY 型は WGS84 上の点・線・多角形を表し、ST_GEOGPOINT(経度, 緯度) を起点に ST_DISTANCE・ST_DWITHIN・ST_INTERSECTS・ST_CONTAINS を使い分けて最寄り検索・エリア集計を組み立てる
- APPROX_COUNT_DISTINCT と HLL++ スケッチは、正確な COUNT(DISTINCT) と引き換えに速度とストレージ効率を得る近似集計の手段。precision で誤差とコストを調整できる
- HLL スケッチは事前集計して MERGE で再集計できる点が本質。日次スケッチを保存しておけば任意の期間集計を生ログの再スキャンなしで求められる
- BigQuery Studio ノートブックと BigQuery DataFrames は SQL と Python を橋渡しする仕組みだが、Compute Engine の VM ランタイムを使うため課金プロジェクトが前提
- 次章ではセキュリティとガバナンス(IAM・行列レベルセキュリティ・監査ログ)を扱い、ここまで作ってきたデータ基盤を安全に運用する視点を補います