1. はじめに
先日の ClickHouse のもくもく会で、ある参加者が作っていたアプリが面白そうでした。サンプルデータの世界中の携帯基地局を3Dの地球儀(グローブ)の上にプロットして、ぐりぐり回せるというものです。しかも「コードは1行も自分で書かず、全部 Claude Code に作らせた」とのことでした。元ネタは AWS と ClickHouse で実装したこちらの記事にまとめられてます1。素直に面白かったので、自分でも作ってみたいと思いました。
自分の環境では、WSL から Google Cloud も ClickHouse も触れます。そこで「AWS の部分を Google Cloud に置き換えたら、同じものが Claude Code でどこまで作れるか」を試してみました。アプリの基本的な作り ―― cell_towers を H3 で集約して deck.gl のグローブに描く、点群と六角形の 2 モード、画面にスキャン行数を出す、といった設計 ―― は元記事1を踏襲しています。そのうえで、AWS だった部分(Amplify / API Gateway / Lambda)を Google Cloud(Cloud Run)に置き換え、データストアは手元の ClickHouse(以下 CHTEST と表記)をそのまま使いました。コードは Claude Code に生成させています。どこを元記事から借り、どこを自分で変えたかは、各章でそのつど明記します。
先に結論です。
- GCP の Cloud Run + ClickHouse で、約4,327万件の基地局データを3Dグローブに描画できた。radio(通信方式)と H3 解像度を切り替えられ、六角形に集約した表示と、個々の基地局を点で出す「点群」表示を行き来できる
- ほぼ Claude Code 任せで完成した。アプリのコードは自分ではほとんど書いていない
- 速い。H3 で事前集約しておいたので、デフォルトの解像度ではブラウザ操作から表示まで端から端で約 80 ミリ秒(ClickHouse 側のクエリは約 9 ミリ秒)
- データを眺めると分かることがいくつかあった。たとえば 5G(NR)は世界で 867 件しか入っていない(このデータセット時点)
- つまずいた点も書く。高解像度でレスポンスが Cloud Run の上限を超えた話、H3 関数の引数順、WSL から GCP を操作するときの注意など
完成した画面はこんな見た目です。個々の基地局が点となって、陸地の形が浮かび上がります(元記事と同じ「点群」表示)。
2. 検証環境
| 項目 | 内容 |
|---|---|
| フロントエンド | React 19 / Vite 6 / TypeScript / Tailwind CSS 4 / deck.gl 9(GlobeView + H3HexagonLayer) |
| バックエンド | Node.js 20 / Express / @clickhouse/client |
| 実行基盤 | Google Cloud Run(asia-northeast1) |
| データストア | ClickHouse 26.2.1.474(CHTEST)、cell_towers 43,276,150 行 |
| 実装 | Claude Code(コードは全て生成) |
cell_towers は ClickHouse 公式のサンプルデータセットで2、世界中の基地局の位置(緯度・経度)と通信方式などが入っています。通信方式(radio 列)の内訳は次のとおりでした。
| radio | 件数 |
|---|---|
| UMTS(3G) | 20,686,487 |
| LTE(4G) | 12,101,148 |
| GSM(2G) | 9,931,304 |
| CDMA | 556,344 |
| NR(5G) | 867 |
3. 構成・実装
3.1. 全体構成
元記事は AWS Amplify(フロント配信)と API Gateway + Lambda(バックエンド)でしたが、今回は Cloud Run 1 つにまとめました。1 つのコンテナで、ビルド済みのフロント(静的ファイル)も /api も同じオリジンで配信します。
なぜ Cloud Run にしたか。コンテナをそのまま動かせて、リクエストが無い間はゼロまでスケールするので普段は課金されません。API Gateway + Lambda の役割を 1 サービスに集約でき、デプロイも gcloud run deploy --source の 1 コマンドで済みます3。フロントを別サービス(Firebase Hosting など)に分ける手もありましたが、今回は構成要素を最小にしたかったので、同じコンテナから配信する形にしました。
データストアは AWS 上の ClickHouse のままにしました。つまり Cloud Run(GCP 東京)から ClickHouse(AWS 東京)へ問い合わせるクロスクラウド構成です。同一クラウドよりネットワーク的には不利ですが、後述のとおりデフォルトの解像度では体感に影響しませんでした。
3.2. H3 で事前集約しておく
4,327万件をそのまま毎回ブラウザに送るのは現実的ではありません。そこで H3 で事前にまとめました。
H3 は Uber が公開している「六角形による地理インデックス」です4。地球の表面を大きさのそろった六角形のセルで敷き詰め、緯度・経度を渡すと、その点がどのセルに入るかを表す ID(64 ビットの整数)が返ります。四角形のグリッドではなく六角形なのは、どのセルも隣り合う 6 つのセルと中心どうしの距離がほぼ等しく、密度や近さを素直に扱えるためです。
セルの細かさは 解像度(resolution, 0〜15) で決まります。数字が大きいほどセルは小さく、1 段上がるごとにおよそ 7 倍細かくなります。ざっくり言うと、解像度 3 で 1 セルが県〜地方くらい、解像度 6 で市区町村くらいの広さのイメージです。
近くにある基地局は同じセル ID になるので、セル ID ごとに件数を数えるだけで地図用の集計ができます。
セルごとに件数をあらかじめ数えた集約テーブル cell_towers_h3 を作っておき、ブラウザにはセルの中心座標と件数だけを返します。解像度 2〜6 を用意しました。行数は次のとおりです。
| H3 解像度 | 六角形セル数 | 元の基地局数 |
|---|---|---|
| 2 | 5,477 | 43,276,150 |
| 3 | 23,834 | 43,276,150 |
| 4 | 100,624 | 43,276,150 |
| 5 | 389,259 | 43,276,150 |
| 6 | 1,332,941 | 43,276,150 |
集約は ClickHouse の H3 関数で行います5。geoToH3 で緯度経度をセル ID に変換し、h3ToString で deck.gl が扱える文字列形式(例: 832f5afffffffff)にして返しています。
3.3. バックエンドのクエリ
/api/towers は解像度と radio をクエリ文字列で受け取り、型付きプレースホルダでバインドして ClickHouse に発行します。型を指定するのでインジェクションの心配がありません。
SELECT h3ToString(h3) AS h3, sum(cnt) AS cnt,
any(center_lon) AS lon, any(center_lat) AS lat
FROM default.cell_towers_h3
WHERE h3_res = {res:UInt8} AND radio IN {radios:Array(String)}
GROUP BY h3 ORDER BY cnt DESC
3.4. 2 つの表示モード(六角形 / 点群)
表示は 2 モードを用意しました。六角形(集約)モードは、ここまでの cell_towers_h3 を引いて H3 セルごとの件数を出します。広い範囲を俯瞰するとき向きで、軽いのが利点です。もう一つの 点群(raw)モードは、集約せずに個々の基地局を 1 点ずつ描きます。生データ cell_towers を直接読むため重くなりますが、点の散らばりがそのまま見えます。元記事がズームインで生の点に切り替えていたのと同じ考え方で、左上のボタンで切り替えられます。
4. 手順・実行
Claude Code に投げて、おおむね次の順で進みました。
-
データ準備: CHTEST に
cell_towers_h3(解像度 2〜6)を作る。43万件×複数解像度の集約は時間がかかるので、ここだけは別途まとめて実行した -
バックエンド: Express で
/api/towersと/api/health、それに静的 SPA 配信を書く -
フロント: React + deck.gl の GlobeView に H3HexagonLayer を載せ、
/api/towersを取得して描画。右上に「⚡ N 行スキャン / X ms」を出す -
デプロイ:
gcloud run deploy --sourceでコンテナをビルドして Cloud Run へ - 確認: ブラウザで開いて、解像度・radio を切り替えながら見る
左上のパネルで解像度(スライダー)と radio(チェックボックス)を切り替えられます。
5. 動かした結果
5.1. 点群(raw)モードで全体を見る
冒頭の画像が、この「点群(raw)」モードで世界全体を出したものです。集約をやめて個々の基地局を点で描き、radio で色分けしています。右上には「⚡ 37,478,400 rows scanned / 1042 ms ・ 250,000 pts」と出ています。
このモードは集約テーブルを使わず、生の cell_towers(約4,327万行)を直接読んで点を間引いて返します。そのため「rows scanned」が数千万件になり、後述の六角形モード(事前集約で 1 桁ミリ秒)より時間がかかります。点が多すぎて重くならないよう、ここでは全体から 25 万点をサンプリングして返しています。元記事がズームインで生の点に切り替えていたのと同じ振る舞いです。
5.2. 六角形(集約)モードと3D表示
広い範囲を軽く俯瞰したいときは「六角形(集約)」モードです。H3 セルごとの件数を六角形にして表示します。
拡大して傾けると、六角形が件数に応じて高さを持つ柱になります。マウスを当てると件数が出ます。下は東京付近を拡大したところで、ツールチップに「32,230 towers」と出ています。
5.3. 解像度を変える
解像度 2 は大きな六角形でざっくり、解像度 5 まで上げると海岸線や都市の形がくっきり出ます。
5.4. 通信方式で切り替える
radio を切り替えると、通信方式ごとの分布の違いが見えます。GSM(2G)と LTE(4G)でも広がり方が違います。
そして NR(5G)だけにすると、画面はほぼ空になります。このデータセットには 5G が 867 件しか入っていないためで、世界に数クラスタしか出ません。
5.5. 速度
各解像度での所要時間です。elapsedMs は ClickHouse 側のクエリ実行時間、client_total はブラウザ操作から結果が返るまで(クロスクラウドのネットワーク往復を含む)です。
| H3 解像度 | ClickHouse 側 | 端から端 | 返した六角形の数 |
|---|---|---|---|
| 2 | 6 ms | 0.05 s | 1,922 |
| 3(デフォルト) | 9 ms | 0.08 s | 9,062 |
| 4 | 19 ms | 0.23 s | 40,811 |
| 5 | 73 ms | 1.09 s | 171,604 |
| 6 | 約 150 ms | 3.8 s | 約 130 万 |
デフォルトの解像度 3 なら端から端で約 80 ミリ秒で、回しても引っかかりません。解像度を上げると遅くなりますが、その大半は ClickHouse のクエリ時間ではなく、返す六角形の数(JSON)が増えて転送に時間がかかるためです。
6. 考察・つまずいた点
6.1. H3 の事前集約で軽くなった
解像度 3 でブラウザに返す行は 9,062 件です。元の 4,327 万件を毎回集計して全部返すのに比べれば桁違いに小さく、これが対話的に回せる理由です。元記事が「4,327万行を 0.18 秒で集計」と紹介していたのは事前集約なしの全件集計の話で、こちらは集約テーブルを引くので ClickHouse 側は 1 桁ミリ秒で返ります。
6.2. H3 関数の引数順
最初に geoToH3(経度, 緯度, 解像度) の順で渡したら、東京を入れたのに中心が太平洋の上に出ました。このバージョン(26.2)では geoToH3(緯度, 経度, 解像度) の順(緯度が先)で渡すと正しい位置になりました。cell_towers の列は経度・緯度の順なので、呼び出しでは入れ替えて渡しています。h3ToGeo の戻り値も (latitude, longitude) の順でした。
6.3. 高解像度でレスポンスが上限超え
解像度 6 を選ぶと、最初はブラウザで「Unexpected end of JSON input」になりました。解像度 6 は世界で約 130 万の六角形になり、JSON にすると 100 MB を超えます。これが Cloud Run の HTTP レスポンス上限(32 MB)を超えて途中で切れていました。対処として、レスポンスを gzip / br で圧縮し、返す六角形を件数の多い上位 30 万に絞りました。圧縮後は数 MB に収まり、解像度 6 も表示できるようになりました。
解像度 6 で返すのは「全 130 万のうち件数上位 30 万」です。1 本だけ立っているような疎な地域は間引かれます。全世界を最細密で一度に出すのは用途的にも重いので、ここは割り切っています。
7. まとめ
AWS で公開されていた「全部 Claude Code で作る3Dグローブ」を、Google Cloud(Cloud Run)と ClickHouse で再現してみました。データストアはそのままに、フロントとバックエンドを GCP に載せ替える形で、ほぼコードを書かずに動くところまで到達できました。H3 で事前集約しておけば 4,327 万件でも対話的に回せること、高解像度ではレスポンスサイズが先に問題になること、H3 関数の引数順といった細かい引っかかりも分かりました。
きっかけをくれたもくもく会の参加者と、元記事1に感謝します。
なお「そもそも、なぜ ClickHouse だとこれが速いのか」を、同じ 4,327 万件を Postgres に積んで確かめた話は、別記事にまとめる予定です。
8. 参考
-
参考にした元記事(@Syoitu さん、AWS + ClickHouse で実装)。本記事はこのアプリ設計を踏襲し、AWS 部分を Google Cloud(Cloud Run)に置き換えて再現したものです。https://qiita.com/Syoitu/items/d463c58d15db90303392 ↩ ↩2 ↩3
-
ClickHouse ドキュメント「Cell Towers」サンプルデータセット。https://clickhouse.com/docs/getting-started/example-datasets/cell-towers ↩
-
Google Cloud ドキュメント「Deploy from source code」(
gcloud run deploy --source)。https://cloud.google.com/run/docs/deploying-source-code ↩ -
H3(Uber 開発の六角形による地理インデックス)公式サイト。https://h3geo.org/ ↩
-
ClickHouse ドキュメント「H3 Indexes」関数(
geoToH3/h3ToGeo/h3ToStringなど)。https://clickhouse.com/docs/sql-reference/functions/geo/h3 ↩








