SpannerにおけるMutationの数え方

この記事は 【[UniposAdventCalendar2021](https://qiita.com/advent-calendar/2021/unipos)】 の17日目の記事です．

SpannerのMutationの数え方を調べた時に勉強したことをまとめました

# Mutation
MutationはDBへの操作（Insert・Update・Delete）の単位として使われる言葉

Transactionを使ってアトミックに複数のMutationをまとめて実行する際には20kMutation問題を意識する必要がある．
20kMutation問題とは，一つのTransactionで扱えるMutationが2万個までという制限のこと．

# Mutationの罠
spannerパッケージにあるspanner.Mutationはコード上でInsertやUpdateなどの処理の単位を表しているが， Spannerの内部ではもっと細かい処理の単位に分解されるらしく， コード上で扱うspanner.Mutationの数を数えても20k制限対象のMutationの数とは一致しない．コードのMutationの数が20000個以下ならOKとかそういうことではない．

```golang
func write(w io.Writer, db string) error {
        ctx := context.Background()
        client, err := spanner.NewClient(ctx, db)
        if err != nil {
                return err
        }
        defer client.Close()

        singerColumns := []string{"SingerId", "FirstName", "LastName"}
        albumColumns := []string{"SingerId", "AlbumId", "AlbumTitle"}
        m := []*spanner.Mutation{
                spanner.InsertOrUpdate("Singers", singerColumns, []interface{}{1, "Marc", "Richards"}),
                spanner.InsertOrUpdate("Singers", singerColumns, []interface{}{2, "Catalina", "Smith"}),
                spanner.InsertOrUpdate("Singers", singerColumns, []interface{}{3, "Alice", "Trentor"}),
                spanner.InsertOrUpdate("Singers", singerColumns, []interface{}{4, "Lea", "Martin"}),
                spanner.InsertOrUpdate("Singers", singerColumns, []interface{}{5, "David", "Lomond"}),
                spanner.InsertOrUpdate("Albums", albumColumns, []interface{}{1, 1, "Total Junk"}),
                spanner.InsertOrUpdate("Albums", albumColumns, []interface{}{1, 2, "Go, Go, Go"}),
                spanner.InsertOrUpdate("Albums", albumColumns, []interface{}{2, 1, "Green"}),
                spanner.InsertOrUpdate("Albums", albumColumns, []interface{}{2, 2, "Forever Hold Your Peace"}),
                spanner.InsertOrUpdate("Albums", albumColumns, []interface{}{2, 3, "Terrified"}),
        }
        _, err = client.Apply(ctx, m)
        return err
}
```

引用: https://cloud.google.com/spanner/docs/modify-mutation-api

ドキュメントにも挿入や更新などの操作はカラムの数・インデックスの数に影響されると記載されている
>  挿入と更新のオペレーション回数は、オペレーションの影響を受ける列数を単位としてカウントされ、主キー列は常に影響を受けます。たとえば、5 つの列に値を挿入すると、新しいレコードの挿入は 5 つのミューテーションとしてカウントされます。
引用: https://cloud.google.com/spanner/quotas#note2

# Mutationの数え方
最近の[GCPUGの動画](https://youtube.com/playlist?list=PLClZJ7TJX2gEYOfcA1UU-S-5VKsqCFXuO)でしんめたるさんが解説をしてくれていたり， [apstndbさんの記事](https://github.com/gcpug/nouhau/tree/master/spanner/note/mutation-count)が参考になった．
特にapstndbさんの記事では細かい条件で推定したMutation数が書かれている．

ここではザックリとした計算方法と気をつけたいことを書いておく．
以下は全て1レコードに対する操作の場合．

## Insert
> テーブルのカラム数 + テーブルのINDEX数

NullableなカラムがあったとしてもINDEXはNULLで作成されるのでMutationとしてカウントされることに注意

## Update
> 更新対象のカラム数 + 更新対象のカラムを含むINDEX数×2

しんめたるさんの資料では触れられていなかったが，公式のドキュメントによるとUpdate時でも主キーは必ずMutationに影響を与える． テーブルの更新対象のカラム以外に主キーがある時には， そちらのカウントも忘れないようにする
> ...（前略） 主キー列は常に影響を受けます。たとえば、5 つの列に値を挿入すると、新しいレコードの挿入は 5 つのミューテーションとしてカウントされます。レコードに 2 つの主キー列がある場合、レコード内の 3 つの列を更新した場合も 5 つのミューテーションとしてカウントされます。
引用: https://cloud.google.com/spanner/quotas#note2

## Delete
> 1 + テーブルのINDEX数

# Transactionで実行されたMutation数の取得
細かく条件や状況が変わると正確なMutationを計算するのは難しいので， 実際には動作確認段階でTransaction内で実行されたMutation数は取得しておく方がよさそう．
spanner.Client.ReadWriteTransactionWithOptionsを使ってcommitの統計情報取得すれば， 正常にcommitされた時にMutationの数をTransactionのレスポンスで取得することができる．
↓こんな感じ

```golang
sc, err := spanner.NewClient(ctx, db)
if err != nil {
    return err
}
res, err := sc.ReadWriteTransactionWithOptions(ctx context.Context, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error {
    sqlStr := `INSERT HOGE (ID, Fuga) VALUES (@id, @fuga)`
    stmt := spanner.NewStatement(sqlStr)
    stmt.Params["id"] = id
    stmt.Params["fuga"] = fuga
    rowCount, err := tx.Update(ctx, stmt)
    if err != nil {
        return err
    }
    return nil
}, spanner.TransactionOptions{CommitOptions: spanner.CommitOptions{ReturnCommitStats: true}})
if err != nil {
    return err
}
fmt.Println("%d mutations in transaction", res.CommitStats.MutationCount)
return nil
```

実際にMutation制限に引っかかりそうな処理を書くことになってしまったら， 想定するMutation数と実際に実行されたMutation数を比較して差が出たことを検知できるError Logか何かを仕込んでおいた方がよい． スキーマの変更がコードに反映できていないことがすぐに検知できるし， SpannerのMutationの計算方法が変わったことにも気づけるかもしれない．

また， 想定するMutation数も20kギリギリを攻めるとスキーマが変更されたなどの理由からすぐにオーバーしてしまうし， Spannerの内部処理ががこっそり変更されたことでちょっと増えたりした時に障害が発生するなんてことも考えられるので， 15k程度を目安にしておくといいかもしれない．

とにかく， ドキュメントに明記されていない上に仮説による推定でしかないため， 依存するSpannerの内部処理が変更されてしまったらずれてしまう事を念頭においておかないといけない． 基本的には仮説ベースではなく計測ベースの実測値を使って， 計測結果が変わったら対応を都度考えていく運用が必要そう．

# 追記
STORING INDEXを持つテーブルでInsertのMutationを数える実験を行ってみた結果、どうやら上記の式だけでは説明ができない．
実際にはSTORINGしている値に対してもMutation操作が発生しているっぽいので注意が必要．

Insertは今のところこんな感じの仮説を立てると辻褄があう
> テーブルのカラム数 + テーブルのINDEX数 + テーブルの全INDEXがSTORINGで格納しているカラムの総和

STORINGしている値に対してもInsertが必要なのでその分Mutationが膨らむはずという読み．
UpdateやDeleteにどれくらい影響するのかはわからないが、UpdateではSTORINGに対する削除と挿入の2mutationがたされるかもしれないと思っておくとよさそう

# 参考 
- https://cloud.google.com/spanner/docs/modify-mutation-api
- https://pkg.go.dev/cloud.google.com/go/spanner#Mutation
- https://cloud.google.com/spanner/quotas#note2
- https://youtube.com/playlist?list=PLClZJ7TJX2gEYOfcA1UU-S-5VKsqCFXuO
- https://cloud.google.com/spanner/docs/commit-statistics