Spanner Mutation
前回Spannerについて発表したときに話きれなかったMutationについてまとめたい
サポートしている機能に差がある:参考
SQL 構文、データの挿入または無視、書き込み後読み取り(RYW)、制約チェックに差分があるぽい。ここはそこまで驚きはないかも。
詳細
DML とミューテーションは、次の機能のサポートに違いがあります。
書き込み後読み取り: 有効なトランザクション内で commit されていない結果を読み取ります。DML ステートメントで行った変更は、同じトランザクション内の後続のステートメントで参照できます。これはミューテーションの使用とは異なります。ミューテーションでは、トランザクションが commit されるまで、すべての読み取り(同じトランザクションで行われた読み取りを含む)では変更を参照できません。トランザクション内のミューテーションはクライアント側で(ローカルで)バッファリングされており、commit オペレーションでサーバーに送信されるためです。このため、commit リクエストのミューテーションは、同じトランザクション内の SQL ステートメントまたは DML ステートメントで参照できません。
制約チェック: Spanner では、すべての DML ステートメントの後に制約がチェックされます。ミューテーションを使用した場合、commit するまでクライアントのバッファにミューテーションが保存され、commit 時に制約のチェックが行われます。各 DML ステートメントの後で制約を評価することで、同じトランザクション内の後続のクエリによって返されたデータがスキーマと整合するデータを返すことが保証されます。
SQL 構文: DML は、従来の方法でデータを操作できます。SQL スキルを再利用することで、DML API を使用したデータの変更が可能です。
単純に行数を数えるとかそういう感じではないらしい。内部実装がどうなるとこうなるんだ...?
バッチ書き込み という機能があるの全然知らなかった Pre-GAっぽい
Spanner のバッチ書き込みを使用すると、Spanner テーブルで複数行の挿入、更新、削除を実行できます。Spanner のバッチ書き込みは、読み取りオペレーションのない低レイテンシの書き込みをサポートし、ミューテーションがバッチに適用されるときにレスポンスを返します。バッチ書き込みを使用するには、関連するミューテーションをグループ化します。グループ内のすべてのミューテーションはアトミックに commit されます。グループ間のミューテーションは不特定の順序で適用され、互いに独立しています(非アトミック)。Spanner は、すべてのミューテーションが適用されるのを待たずにレスポンスを送信できます。つまり、バッチ書き込みでは部分的な失敗が許容されます。複数のバッチ書き込みを一度に実行することもできます。詳細については、バッチ書き込みの使用方法をご覧ください。
なんだかよくわからないが、いくつかの変更を並列に実行し、一部が成功みたいなステータスがありうるものっぽい
バッチ書き込みを使用してデータを変更する を読むのが良さげ。MutationGroupってのを定義して書き込みを行う。それぞれの書き込みに対しエラーレスポンスが帰ってくるみたい。
BatchWrite と BatchUpdateは別物
GCP Cloud SpannerのBatchWriteを触ってみた
「一部だけ適用される可能性がある」というところがめちゃくちゃ難しい気がする。一部だけ適用されなかった際に、どう対処すべきかは考えないといけない。一部だけリトライする事自体は可能だがそれでよいのか。副作用がなく安全な処理に使うのが望ましいか。そうなると期限切れデータの削除とかになるがそれってMutationでやるものではない気も。
実際にmutationをやっているブログ Cloud Spanner:ミューテーションによるバッチ書き込み方法
こっちがBatch Write
Mutation機能の制約についても考えておきたい
現状見えているものは
- 書き込んだものがコミット完了まで参照できないため、Txの中で同一テーブルにSELECTを投げるような処理がある場合には利用を推奨しない
- SELECTのみならず、UPDATEやDELETE DMLとの混在も推奨されない。クエリの実行順序が保証されない。例えばTxの中であるレコードをINSERT (by Mutation)→Update (by DML) とした場合にはUPDATEに失敗するはず。
- Mutationは1TxまたはMutation Groupのなかで80kまでという所謂80k Limitがある
- DBの制約がコミット時まで評価されないため、(ほとんどの可能性においてないと言ってよいが、)コミットするまでテーブルにデータが収まるかわからないことがあるのかも。文字列の最大長等?入力時にバリデーションチェックをかければ基本問題ないはず。
- 同時実行制御が難しくなる。これが一番のデメリットかも。SELECT FOR UPDATE 系のロック制御を行うことで制御できるが、不要なロック待ちを増やすリスクもありあまり推奨できない気がする。ユースケースに応じて慎重に検討する必要がある。
また、
- 上記の仕様によりDMLとの使い分けが推奨されていない。使い分ける場合にはどちらを使っているか明示的に意識する必要があり、コードがCleanに保ちにくいためClean Architectureとの相性も悪くなりそう(Mutationによる変更かDMLによる変更なのかをUsecase層で意識しないといけなくなる)
- 結果として、1つでもDMLを用いるものがある場合には基本的にすべてDMLにする場合が望ましいのではないか...。
本題とはちょっとずれるかもしれないが、高速化においてはこのベストプラクティスも抑えたい
ざっくり抜粋
- 読み取り / 書き込みトランザクションで大量の読み取りを回避する。
- ロックを取得しないため、可能な限り読み取り専用トランザクションを使用します。
- 読み取り / 書き込みトランザクションでのテーブルのフルスキャンを回避します。
- データが読み取り / 書き込みトランザクションで読み取り可能になった直後に変更を commit することで、ロック期間を短縮できロックの競合が発生する可能性が低くなります。
- 長いロックは避ける - 大規模なトランザクションよりも小規模なトランザクションを優先するか、長時間実行の DML トランザクションのパーティション化 DML を検討してください。
- 読み取り / 書き込みトランザクションが提供している保証が不要な場合は、読み取り / 書き込みトランザクションでデータを読み取ってから変更を commit するという操作を行わないでください。たとえば、別の読み取り専用トランザクションでデータの読み取りを実行してください。
- ロックするカラムの絞り込み - 読み取り / 書き込みトランザクションに必要な最小限の列セットのみを指定します。Spanner ロックはデータセルごとに行われるため、読み取り / 書き込みトランザクションで列を読み取る量が多すぎると、これらのセルに対する ReaderShared ロックが取得されます。これにより、他のトランザクションが過剰な列への書き込みで WriterShared ロックを取得するときに、ロック競合が発生する可能性があります。たとえば、読み取り時に * ではなく一連の列を指定することを検討してください。
- 読み取り / 書き込みトランザクションでの API 呼び出しを最小限にします。※おそらく外部APIの呼び出しのこと
- スキーマ設計のベスト プラクティスに従ってください。
スキーマ設計のベスト プラクティス
- ホットスポットを防ぐように主キーを選ぶ
- タイムスタンプ キーの降順での格納(現在から特定の時間まで遡るさい、こちらのほうが効率的)
- インターリーブ インデックスを使用する場合 ←ここわからなかった。なぜインターリーブを使うと良いのか...?
- インターリーブ インデックスを使用する場合 ←ここわからなかった。なぜインターリーブを使うと良いのか...?
ここは気になる
プラクティスいろいろ
Batch updateについても
何番目のStatementでエラーが出たとかわかるんだっけ(わかるはず...)