🏃

MySQLユーザーによるSpannerキャッチアップへの道

に公開2

はじめに

こんにちは。カナリーでソフトウェアエンジニアをしている村山です。

カナリーではRDBとしてGoogle CloudのSpannerを利用しています。Spannerはスケーラブルかつ、強力な整合性および一貫性を持つデータベースで、国内でも多くの採用実績があります。

私自身、Spannerを利用するのはカナリーがはじめてでして、これまでもMySQLの利用がほとんどでした。そんな私がSpannerをキャッチアップするためにやったことを今回ご紹介します。

Spannerの良さを学ぶ

そもそもSpanner自体をあまり良くわかっていないので、公式の謳い文句を確認します。

https://cloud.google.com/spanner?hl=ja#common-uses

リレーショナル、グラフ、Key-Value、検索を組み合わせた単一のデータベースを使用して、インテリジェントなアプリを構築します。メンテナンスの時間枠がないため、ミッション クリティカルなアプリが中断されません。

これを見ると、SpannerはどうやらRDBとしての利用に加えて、用途としてグラフやKV的なアクセスにも対応しているようです。計画停止が不要で、常に運用できる点も魅力です。

実際にSpannerを導入し、その恩恵を得ている企業の話も参考になります。NTTデータでは、決済代行ソリューション「Omni Payment Gateway」において、Cloud SQL for MySQLからSpannerに移行したようです。MySQLからの移行という点でなんだか親近感が湧きますね。

https://www.youtube.com/watch?v=iAFYP62Pl88

移行の必要性に「メンテナンスの停止・決済の大幅増加見込み・可用性要件」とあり、決済関連のプロダクトということも含めて、Cloud SQLから脱却することが求められていたようです。

上記動画はSpanner導入にあたっての検討事項や移行手順を詳しく紹介しており、非常に参考になるためぜひご覧ください。

前述のドキュメントや関連する動画を見ていると、Spannerは高額なデータベースでありながらも、それに見合った充実した機能を提供しているようです。最初はスモールスタートで他のデータベースを利用しつつ、スケールしていくにしたがってSpannerに移行するという選択肢も十分有り得そうですね。

その他のメリットもまだまだたくさんありそうですが、今回はあくまでキャッチアップを優先したいので、良さを学ぶのはこれくらいにしようと思います。

自分のDB知識を再確認する

具体的なキャッチアップへ入る前に、自分が今持っている知識や経験を確認します。この確認をすることで自分に足りていないものを認識できるので、キャッチアップがより効率的になります。もし基本的なSQLの書き方すら怪しければ、Spannerの勉強をする前にもっと初歩的なDBの勉強をしよう、といった判断ができるからです。

私の場合は以下のとおりです。

できること

  • MySQLの一般的な利用
    • テーブルの新規作成・カラム追加・インデックス張り付け
    • 実行計画を見ながらのクエリチューニング
    • テーブルロックを回避しながらのオンラインDDL実施
  • SQLの基礎知識

不足していること

  • DB内部のアーキテクチャ理解
    • 中身がどうなっているかはよくわからない
  • 他RDBMSの実務経験
    • MySQL以外はほぼ未経験
  • Google Cloud の利用経験
    • BigQueryを少し触った程度でgcloud CLIの使い方もわからない

したがって私の立ち位置としては「MySQLは最低限扱えるけど、DBの深い知識は持っていない。Spannerに関しては完全な初心者」といった感じでしょう。本来ならじっくりとDBそのものの深い知識を養いたいところではありますが、まずは現場で使っているSpannerをキャッチアップして、チーム開発に貢献したいという思いがあるので、今回はこのキャッチアップを優先します。

キャッチアップへの道🥋

何はともあれドキュメントを見てみる

キャッチアップの第一歩は、まずは公式ドキュメントを流し見することです。最近、私はなにかの技術をキャッチアップするときは優先して公式ドキュメントを利用します。内容が丁寧で詳しいし、何より情報が正確なので勉強にもってこいです。人気のOSSやクラウドサービスのプロダクトだと、世間の学習教材に匹敵するくらい中身が充実していることもあります。

Spannerのドキュメントページは下記にあります。

https://cloud.google.com/spanner/docs/editions-overview?hl=ja

ざっと内容を確認すると、確かに必要な情報は揃っていそうです。ところが、いわゆるチュートリアルに該当しそうな「Google Cloud コンソールを使ってデータベースを作成してクエリを実行する」ページを見ると、いきなりGoogle Cloud上にSpannerインスタンスを立てる方法が紹介されています。私としてはローカルで好きにいじっていい環境を作って、それを利用して学習したかったので、別の方法で学習を進めることとします。

Spannerではローカル開発用にエミュレーターが提供されています。このエミュレーターを構築するページを見つけたので、次項ではこのページを参考に環境構築のやり方を説明します。

ローカルにエミュレーターを立てる

千里の道も一歩から。まずはローカルで好きにいじっていい環境を準備しましょう。実際にGoogle Cloud上にSpannerのインスタンスを立てて勉強するのもいいですが、ローカルのほうが気兼ねなくいじれて安心ですし、普段の開発でもローカルでエミュレーターを使うことが多いと思うので、今回ご紹介します。

https://cloud.google.com/spanner/docs/emulator?hl=ja

下記の手順は公式ドキュメントに則ったものです。目新しい内容はありません。注意点として、「Dockerおよびgcloud CLIを利用できること」が前提にあります。幸い私はDockerの利用経験があったため、この学習はスキップして良さそうです。gcloud CLIに関してはほぼ何も知らなかったので、事前にちょっと勉強しておきました。ちなみにこのgcloud CLIの勉強も苦戦しまして、使っているうちに自然と覚えていくだろうと、最初はたかをくくっていたのですが、途中から諦めてしっかり勉強することにしました。無理な近道は禁物ですね。

構成を追加する

まずはローカルにエミュレーターを立てるための構成を用意します。gcloud configの利用方法を詳しく知りたい方は公式ドキュメントをご確認ください。

# ローカル用の構成を新規作成する
gcloud config configurations create <設定名> 
# 以下は上記設定に対して追加の設定を行う
gcloud config set auth/disable_credentials true # 認証をOFFにする(エミュレーターは認証を受け付けないので設定しないとエラーになる)
gcloud config set project <プロジェクト名> # ローカル用なので適当でOK
gcloud config set api_endpoint_overrides/spanner http://localhost:9020/ # Spanner APIのエンドポイントをローカルエミュレーターに変更する

下記は実行例です。

gcloud config configurations create local-emulator 
gcloud config set auth/disable_credentials true 
gcloud config set project test-project
gcloud config set api_endpoint_overrides/spanner http://localhost:9020/ 

これで必要な構成を追加できます。次は実際にエミュレーターを立ち上げます。

エミュレーターを立ち上げる

下記を実行すると、コンテナが立ち上がり、エミュレーターが起動します。簡単ですね。

gcloud emulators spanner start

このコマンドは、内部でエミュレーターのコンテナをフォアグラウンドで立ち上げています。コンテナ内部の標準出力・標準エラーをそのまま流します。以降のコマンドを実行し、うまくいかなかった場合は出力されるログを見てみると、解決の糸口が見つかるかもしれません。

docker psを実行すると、起動中のコンテナを確認できます。

$ docker ps
CONTAINER ID   IMAGE                                           COMMAND                   CREATED          STATUS          PORTS                                                NAMES
425908139f85   gcr.io/cloud-spanner-emulator/emulator:1.5.28   "./gateway_main --ho…"   11 minutes ago   Up 11 minutes   127.0.0.1:9010->9010/tcp, 127.0.0.1:9020->9020/tcp   angry_morse

とは言え、まだ環境の初期段階を構築したに過ぎません。実際のDBに触れるまで、もう少し作業が必要です。

インスタンス作成からDB作成まで

下記でインスタンスを作成します。

gcloud spanner instances create <インスタンス名> \
 --config=emulator-config \ # 必須項目。エミュレーターで起動する場合はこの固定値でOK
 --description="Test Instance" \  # 必須項目。インスタンスの説明文
 --nodes=1 # ノード数
 --project=<作成済みのプロジェクトID> #すでに用意しているプロジェクトを指定する

下記は実行例です。

gcloud spanner instances create test-instance\
 --config=emulator-config\
 --description="ローカル検証用のインスタンス"\
 --nodes=1\
 --project=test-project

インスタンスを作成できたので、次はいよいよDBを作成します。

gcloud spanner databases create <作りたいDB名> --instance=<作成済みのインスタンス名>

実行例です。

gcloud spanner databases create test-db --instance=test-instance

さて、これでDBを作成できました。次項ではテーブルを作成し、データを入れたり、SELECT文で取得したりしてみます。

テーブル作成からデータの挿入と取得まで

下記を実行してテーブルを作成します。

gcloud spanner databases ddl update test-db \
  --instance=test-instance \
  --ddl="CREATE TABLE users ( \
    user_id   INT64         NOT NULL, \
    user_name STRING(100)   NOT NULL, \
    created   TIMESTAMP     NOT NULL DEFAULT (CURRENT_TIMESTAMP()) \
  ) PRIMARY KEY (user_id)"

無事にテーブルが作成できたら、データを挿入してみます。

gcloud spanner databases execute-sql test-db \
 --instance=test-instance \
 --sql='INSERT INTO users (user_id,user_name) VALUES(1,"murayama")'

挿入に成功したら、データが取得できるか確認しましょう。

gcloud spanner databases execute-sql test-db \
 --instance=test-instance \
 --sql='select * from users'

取得できました!

$ gcloud spanner databases execute-sql test-db \
 --instance=test-instance \
 --sql='select * from users'
user_id  user_name  created
1        murayama   2025-07-05T06:21:42.613893Z

これで最低限ローカルのエミュレーターを検証する環境が整いました。しかし、やはりgcloud spannerコマンドを経由してDBを操作するのは少し面倒です。そのため、次章ではSQLクライアントであるDBeaverを使ってエミュレーターにアクセスしてみます。

SQLクライアントでアクセスする

今回利用するSQLクライアントにDBeaverを採用します。なお、他のSQLクライアント(Sequel proなど)でもエミュレーターにアクセスできるかどうかは検証していませんので、あらかじめご了承ください。

DBeaverは下記からダウンロードできます。DBeaver Communityバージョンで問題ありません。

https://dbeaver.io/

ダウンロードが完了した後、⌃⇧Nで「新しい接続」を開きましょう。下記の画面が表示されるはずなので、「Google Cloud Spanner」を選択し、「次へ(N)」を押しましょう。

接続タイプを選択する

次の画面では、作成済みのProject・Instance・Databaseを入力しましょう。

コネクションを編集する

次にドライバーのプロパティタブに移動します。ドライバーのインストールが済んでいない場合はインストールをしてください。インストール後はautoConfigEmulatortrueにします。これにより、DBeaverはエミュレーターに対して自動的にコネクション確立を試みるようになります。

ドライバーのプロパティを編集する

以上の設定が終わったら、終了ボタンを押し、接続できるか試してみましょう。

接続画面

無事に接続できました!これでGUIを通して好きなだけエミュレーターをいじれますね! ここまで来たら勝ったようなものです。

番外編 : spanner-cliを使おう

spanner-cliはターミナル上でSpannerと対話的にSQLでやりとりできるツールです。非常に使い勝手が良く、私も業務で日常的に利用しています。キャッチアップという意味では本筋から外れるため、今回は紹介のみにとどめますが、気になる方はぜひ触ってみてください。

https://github.com/cloudspannerecosystem/spanner-cli

なお、使用感については下記のとおりです。だいぶ扱いやすいですね。

export SPANNER_EMULATOR_HOST=localhost:9010                                               
❯ spanner-cli -p test-project -i test-instance -d test-db                                    
Connected.
spanner> show databases;
+----------+
| Database |
+----------+
| test-db   |
+----------+
1 rows in set (0.01 sec)

なお、最近は公式のSpanner CLIがリリースされるという情報が入っています。現時点ではまだプレビューの段階のようです。この件については下の記事で説明されているため、今回紹介します。

https://zenn.dev/apstndb/articles/spanner-cli-contributor-perspective

プロジェクトで使われているSQL方言を調べる

さて、これまではローカルにエミュレーターを立てて、そこにSQLクライアントでアクセスする方法について述べました。そのエミュレーターをさっそくいじってみてもいいのですが、ちょっと立ち止まってみます。

Spanner未経験者の方はまず、自分が参画しているプロジェクトにおいて、SpannerのSQL方言に何を利用しているのかを調べることをおすすめします。Spannerでは「Google 標準 SQL」あるいは「PostgreSQL」を利用できます。どちらも同じSQLですが、言語として微妙に差異があるため、これを早い段階で認識しておくと間違ったSQLを実行してエラーに悩まされる、といったことがなくなります。とくにMySQL出身のエンジニアはこのあたりでつまづくことが多いので(経験談)、今のうちに慣れておくと良いでしょう。

今回はGoogle Cloudのコンソール画面で確認する方法をご紹介します。Spannerのインスタンス一覧画面を開き、対象のインスタンスをクリックします。

インスタンス一覧

選択すると画面が遷移し、インスタンスとそれに紐づくDBが表示されます。そのDBの「言語」列を確認すると「Google 標準 SQL」とあります。これは私の作成したDBが「Google 標準 SQL」を採用しているためです。

DB一覧

前述のとおり、SQL方言はDB作成時に決定されます。そのとき指定したものがここに表示されます。このDBでは「Google 標準 SQL」が使われているので、具体的なSQLの書き方を知りたい場合は下記ドキュメントを参考にすれば良さそうです。

https://cloud.google.com/spanner/docs/reference/standard-sql/overview

ちなみにPostgreSQLのドキュメントは下記のとおりです。
https://cloud.google.com/spanner/docs/reference/postgresql/overview

設定されているSQL方言に馴染みがない場合は、まずは上記ドキュメントを見て書き方の差分を吸収するのが良いでしょう。

プログラムからDBを操作する

いよいよプログラムからDBを操作します。すでに現場でSpannerを利用している場合は、その現場での書き方を真似するだけで最初は十分かと思います。一方で自分の手でゼロからコードを書いてみるといろいろな学びがあるため、今回はあえてチャレンジしようと思います。

チャレンジと言っても、公式ドキュメントでクイックスタートが公開されているため、これを参考に進めようと思います。ドキュメントには各言語での利用方法が記載されているので、プロジェクトで利用されている言語のページを読めば良さそうですね。

今回はGo用のドキュメントを利用します。Goを経由してエミュレーターにデータを挿入し、そのデータを取得しようと思います。

https://cloud.google.com/spanner/docs/getting-started/go?hl=ja

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
)

func main() {
	db := "projects/test-project/instances/test-instance/databases/test-db"

	ctx := context.Background()

	dataClient, err := spanner.NewClient(ctx, db,
    // 以下、エミュレーター用の設定
    option.WithEndpoint("localhost:9010"), 
		option.WithoutAuthentication(),
	)
	if err != nil {
		log.Fatalf("Failed to create data client: %v", err)
	}
	defer dataClient.Close()

	// データを挿入
	userColumns := []string{"user_id", "user_name"}
	m := []*spanner.Mutation{
		spanner.Insert("users", userColumns, []interface{}{1, "田中"}),
		spanner.Insert("users", userColumns, []interface{}{2, "吉田"}),
		spanner.Insert("users", userColumns, []interface{}{3, "山田"}),
	}

	_, err = dataClient.Apply(ctx, m)
	if err != nil {
		log.Fatalf("Failed to apply mutations: %v", err)
	}

	// データを取得
	stmt := spanner.Statement{SQL: `SELECT user_id, user_name, created FROM users ORDER BY user_id`}
	iter := dataClient.Single().Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			log.Fatalf("Failed to get next row: %v", err)
		}
		var (
			userID    int64
			userName  string
			createdAt time.Time
		)
		if err := row.Columns(&userID, &userName, &createdAt); err != nil {
			log.Fatalf("Failed to get columns: %v", err)
		}
		fmt.Printf("%d %s %s\n", userID, userName, createdAt)
	}

	fmt.Println("done")
}

このコードを実行してみます。

❯ go run main.go
1 田中 2025-07-07 03:21:59.256138 +0000 UTC
2 吉田 2025-07-07 03:21:59.256175 +0000 UTC
3 山田 2025-07-07 03:21:59.256194 +0000 UTC
done

無事データの生成および取得に成功しました!

ただ、大規模なアプリケーションを構築する場合、こういった書き方は実装者によって差分が出てしまいそうですし、同じようなコードの乱立を招く可能性があります。また、型の恩恵を受けることもできません。そんなときはyoというツールの導入を検討しましょう。

yoはSpanner用のGoコードを生成するCLIです。導入方法は下記をご参照ください。

https://github.com/cloudspannerecosystem/yo

今回はこのyoを用いてコードを生成し、そのコードを使って前述の処理を書き直してみようと思います。

yoをインストールした後に、まずは下記schema.sqlを用意してください。これはusersテーブルを生成したときと同じものを記載しています。

CREATE TABLE users (
	user_id INT64 NOT NULL,
	user_name STRING(100) NOT NULL,
	created TIMESTAMP NOT NULL,
) PRIMARY KEY (user_id);

次はいよいよコードを自動生成します。

# 生成先のディレクトリを用意する
mkdir models
# 上記sqlファイルを対象にコード生成を行う
yo generate schema.sql --from-ddl -o models

models配下にファイルおよびコードが生成されました。

yo

この自動生成されたコードを用いて、さきほどのコードを書き換えてみます。

func main() {
	db := "projects/test-project/instances/test-instance/databases/test-db"

	// ...省略
	defer client.Close()

	// データを挿入
	now := time.Now()
	users := []*models.User{
		{UserID: 1, UserName: "田中", Created: now},
		{UserID: 2, UserName: "吉田", Created: now},
		{UserID: 3, UserName: "山田", Created: now},
	}

	var muts []*spanner.Mutation
	for _, u := range users {
		muts = append(muts, u.Insert(ctx))
	}

	if _, err := client.Apply(ctx, muts); err != nil {
		log.Fatalf("Failed to apply mutations: %v", err)
	}

	// データを取得
	ro := client.Single()
	defer ro.Close()
	readUsers, err := models.ReadUser(ctx, ro, spanner.AllKeys())
	if err != nil {
		log.Fatalf("Failed to read users: %v", err)
	}
	for _, u := range readUsers {
		fmt.Printf("%d %s %s\n", u.UserID, u.UserName, u.Created)
	}

	fmt.Println("done")
}

このように書き換えることができました。UserIDおよびUserNameは型指定がされているため、誤った型の値を記載するリスクがなくなりました。また、modelを用いてデータ取得できるようになったことで、取り回しがしやすい状態にもなっています。

コードに規律をもたらしたい、再利用性を高めたい、と思っている場合に、yoは強力なツールになりうるかもしれません。

実行計画を見てみよう

最後のキャッチアップとして、Spannerの実行計画を見る方法について確認します。

実行計画は、Spanner上では「クエリ実行プラン」として下記ページで紹介されています。

https://cloud.google.com/spanner/docs/query-execution-plans?hl=ja

ドキュメントでも記載されているとおり、クエリ実行プランはGoogle Cloudのコンソール上でグラフィカルに表示されます。SQLを実行し、下の「説明」タブを押すと表示されます。

実行計画

クエリの実行と、そのプラン取得を同時に行ってくれるのは親切ですね。

また、下のページでは、実際に実行計画を読み解いてチューニングをする流れを説明しています。これを見ればなんとなく雰囲気を掴めるのではないでしょうか。

https://cloud.google.com/spanner/docs/tune-query-with-visualizer?hl=ja

ただ、グラフィカルに表示されるのは良いものの、正直なところ見方があまり良くわからず、思わずMySQLで使っていた「EXPLAIN」を召喚したくなります。

そう思っていたところ、クエリ実行プランの読み解き方を解説する記事を見つけました。非常にわかりやすい説明で参考になります。

https://zenn.dev/google_cloud_jp/articles/726fcaf614f26e#実行計画の読み解き

また、メルカリ社でも一部Spannerを採用しているらしく、同社テックブログでは実行計画に関する記事がいくつか投稿されています。開発プロセスに組み込むための取り組みも紹介されているので、こちらも参考になりそうです。
https://engineering.mercari.com/blog/entry/20201210-cloud-spanner-query-plan/

SQLを本番環境に直接流すときや、アプリケーションに組み込むときは、実行計画の確認が重要になります。Spannerでも他のDBと同様に確認することができますが、この確認方法が最初わからなかったため、今回ご紹介しました。

キャッチアップのその先へ

これまではSpanner未経験者が最低限キャッチアップするための手順をご説明しました。キャッチアップと言いつつ、紹介してきた内容は初歩中の初歩ですので、実務でSpannerを使った開発をするためにはまだまだ精進が必要です。以下は、私がSpannerにおいて今後学習したいことを記載しています。

まだまだSpannerマスターへの道のりは遠いですが、少しずつ精進していきたいと思います。
弊社ではSpannerを利用したサービス開発を日々行っています。Spannerに関心のある方は、ぜひ弊社のカジュアル面談にお越しください!

Canary Tech Blog

Discussion

apstndbapstndb

「Cloud Spanner の実行計画の活用に関する取り組み」と「spanner-cli コントリビュータが見る公式 Spanner CLI」の著者です。参考になったようで何よりです。

インストール後は autoConfigEmulator をtrueにします。これにより、DBeaverはエミュレーターに対して自動的にコネクション確立を試みるようになります。

java-spanner/java-spanner-jdbc の autoConfigEmulator は Spanner エミュレータへの接続時に必要なインスタンス作成とデータベース作成を自動的に行ってくれるというのも重要ですね。(DBeaver の Spanner サポートは内部的には単に Spanner JDBC Driver を使っているので、他のクライアントでも同様に使えます。)
https://cloud.google.com/spanner/docs/getting-started/jdbc?hl=en#connect_the_jdbc_driver_to_the_emulator

Add autoConfigEmulator=true to the connection URL: This instructs the JDBC driver to connect to the emulator, and to automatically create the Spanner instance and database in the JDBC connection URL if these don't exist.

永続化できない Spanner エミュレータで毎回必要になる「インスタンス作成からDB作成まで」に書かれている手順の多くをスキップして、あとは DBeaver から DDL を投入するだけにもできるかもしれません。

ひかるひかる

コメントありがとうございます!

永続化できない Spanner エミュレータで毎回必要になる「インスタンス作成からDB作成まで」に書かれている手順の多くをスキップして、あとは DBeaver から DDL を投入するだけにもできるかもしれません。

手元で試したら、ご記載の通り多くの手順をスキップできました! 助かります🙇
後ほど本文でも追記させていただきます!