RDBを好む開発者は多い一方、非常に手がかかるシステムの一種です。 どうしても開発者の腕次第でスケーリングや性能、障害の有無に差が出ますし、 事前の綿密な計画が必要な一方、普段は毎日のようにメトリクスを確認してあげないと不安定です。
この記事ではまずはコツコツ修正する部分から始め、実際に起きがちな事象をもとに基本的なことを解説します。
すぐ効いてリスクが少ない: インデックスから設定する
本番やコード影響が少ないのでまずはここから。応急処置です。
🚀🚀 そもそもテーブルにインデックスがない🥺
RDBの扱いが得意な人から見ると絶句されそうですが、どこの組織でも意外とこのレベルの事象が発生してしまっているのが事実です。
この現象は、explainしていない、APIなどのメトリクスを見ていない、といったプロダクト状況を指し示しています。
explainさえすれば、解説をGoogle検索して調べるのは簡単です。大体の場合、1テーブルに最低1インデックスは必要なはず。
どこにインデックスをつけるか、今なら生成AIコードチャットが利用可能です。

- クエリを発行してそうなコードを含んだファイルと
- カラムをまとめているドキュメント、DDLまたはmigrationツールのコード
をコンテキストに投入すればサクッと回答してくれます。これなら簡単でしょう。
最近キャッチアップ不足気味な方用
EXPLAINも良いですがEXPLAIN ANALYZEの方が詳細ですMySQL Workbenchはフェードアウト予定です。MySQL Shell for VS Codeが後継
🚀 複合インデックスがない、間違っている
複数条件で絞り込みたいなら複合インデックスで性能をさらにあげることができます。 MySQLは1カラムだけのインデックスが複数あっても同時に使われません。 複合インデックスは順番も大事。(a,b)はbだけの絞り込みはしてくれません。
絞り込み用のテーブル、巨大テーブルに複合インデックスがないようだと怪しいですね。
AIコードチャットに複合インデックスを作ってと明示すればそれなりのものを作ってくれます。
🚀 join先のテーブルのインデックス注意
1つ目のテーブル(駆動表)と2番目以降のテーブルのインデックスの使われ方は異なることが。MySQLは Nested Join Loop(NJL)という単純なjoinが主です。
1つ目のテーブルはインデックスで絞り込めますが、2番目以降は最初にjoinキーで絞り込みが発生します。
2番目以降のテーブルで絞り込みを追加したいなら、複合インデックスを追加すること。
関数インデックスを検討
文字通り関数であり柔軟で、カラム追加といった本番影響があり得る作業がなくとも絞り込み条件を追加できます。うまくはまれば効果的。
もう少し難しい作業編
若干修正が入ります。
テーブル同士で同一カラムなのに型があってない
たまに発生してます。パフォーマンスの影響が大きいので統一しましょう。
脱TEXT型
数百文字ぐらいならvarcharで十分です。あまり大きいならS3等を検討しましょう。
*詳しい人向け。8.0系の途中からTEXT型もsortなどで内部一時テーブルを使えるようになりました。(公式ドキュメントだと若干分かりにくい)
観察準備と観察フェーズ編
応急処置を終えたら、数値やクエリを見るようにしましょう。フィードバックループを回す上での観察の重要性は様々なところで語られる通り。とはいえ観察には事前準備が必要です。
APMツールでクエリを見る
APMツールならアプリの中でどんなクエリがどんなときにどれくらい発行されているかわかります。まずはセットアップして、定期的に眺めましょう。熟達はまずは生データで修行することから。
おすすめの見方として、全体的にCPUを使うクエリと、重要な動線にあるが遅いクエリを探すと良いでしょう。
全体的にCPUを使うクエリを改善することで、スケーリングの余地を作ります。
重要な動線にある遅いクエリを改善することで、ユーザー体験を向上させます。
APMはアプリケーション側に紐づいているため、よりアプリや顧客目線で改善することができます。APMツールでトレースと紐付けて参照したり、統計的に分析したりすればスムーズに改善できます。
Slow QueryログやDB側のツールはその後で必要に応じて使うぐらいで大丈夫です。
理想を言うと、本番反映前に開発中の段階で確認するのが望ましいです。
*ORMがAS等とクエリを若干加工していることがあります
ステップアップ用チューニングtips
平均ではなく、99%タイルやMAX値を見て一番重い部分から対処しましょう。
- ユーザーのペインが大きい
- リソースや各種プールを占有し他に影響が出る (noisy neighbor)
- カスケード障害の元になりがち
- ピークを元にハードウェア性能を準備することが多いので対処すればコスト削減できる
- 外れ値が平均を引き上げていることがある
ユーザーはレイテンシが安定していることを好みます。遅かったり速かったりするより、安定してやや遅い方が好きです。
クエリ状況を見るタイミング
まず、定期的に見ることから始めましょう。毎日までは望みませんが、隔週やスプリントといった単位で見てみましょう。このとき人が見ているのを眺めるのではなく、自分で操作して問いを浮かべ、そして疑問を解決することで、理解が深まり次の行動につながります。
あとはリリースやマーケティングイベントといった変更が入る時に随時確認しましょう。ワークロードパターンが変われば負荷も変わります。
今回はクエリに着目していますが、APIやRUMなどもっとユーザーに近い方、ビジネスKPIに近い方から見ていくことが望ましいです。
テーブル構造が見えるようにする
本番でコマンド実行しないとわからない状態だと非セキュアかつ非効率です。一部のDBマイグレーションツールだと差分はわかっても現在の全体が見えないという謎の状況に陥ります。特にインデックス。
素直にドキュメントを書くのも一案です。
DB側のメトリクスを見てみる
CPUは当然ですが、一つ挙げるならBuffer Poolのヒット率が大事です。イメージとしてはテーブルを丸々メモリに乗せ、ディスクアクセスなしでメモリだけで完結しているかどうかを示す指標です。保持データによりますが100%を維持しましょう。
負荷を減らしつつ、負荷分散していく
とにかくばらつきを抑えることです。
ここから工数がかかってきます。とはいえプロダクトが成長するならいつかはやらないといけないことです。やらないと悪化することを追い詰められるまでやらないのは悪癖であり、悪いマネジメントでしょう。
* シャーディング / セルアーキテクチャの話はこの記事の範囲を超えるのと、負荷面ではそこまで必要ないことが多いので割愛します。
🚀🚀 ジョブとアプリアクセス先を分ける
ジョブとアプリはワークロードパターンが異なります。全部一緒くたにすると、ジョブがアプリに影響を与えるほか、スケーリングが難しくなります。エンジニアリング的には異なるものは分離するのがよくあるパターンです。ジョブの向き先を隔離しましょう。
何も分散していないならまずはリーダーエンドポイントに、読み取りは分離できているなら、ジョブ専用のカスタムエンドポイントを作り分離しましょう。
微妙に書き込み処理があり、ライターに向けざるを得ないならローカル書き込み転送が便利です。リーダーへのアクセスだけで完結します。具体的には読み取り処理はそのまま、書き込みだけプロキシ的に転送してくれるイメージ。
ジョブはジョブ専用の細かい設定を入れることでパフォーマンスが向上することがあります。やや上級のため、異なる可能性があることだけまずは覚えておいてください。
🚀🚀 ORM / クエリビルダーで一括処理を行う
既存アプリケーションコード、特にモデルを流用してジョブを作成することがあります。そのままだと1件1件処理するため非常に遅くなります。一括処理の機能を探し、それを使いましょう。 パフォーマンス面だけならコード流用よりは直接クエリを書くのがおすすめです。
アスタリスク(*)を控える
不要なカラム取得による負荷もそうですが、何よりカラム追加変更時に障害になる恐れがあります。
SELECT *は本番では避け、利用するカラムを指定しましょう。変更時の影響範囲調査もしやすくなります。
🚀 キャッシュでアクセスを減らす
RDBは綿密なケアが必要です。RDBへのアクセスを減らすことで安定性を保てます。 ブラウザ側、API GatewayやAppSyncといったインフラ側、アプリケーション内のローカルキャッシュと、リクエストのフローを見直してみましょう。前段にいけばいくほど、キャッシュの効果は大きくなります。
RDBの直前にキャッシュサーバーを準備する場合、キャッシュサーバに障害が発生した場合を考慮しましょう。RDBかキャッシュサーバ、どちらかが障害になると障害とすると、直列的に障害発生確率は上昇します。キャッシュサーバはスケーリングというよりは高速化に向いています。
この記事を読むことで必ず力がつきます。
たまにハマるローカルストレージ
JOINやソート時には一時的なデータ置き場が必要です。 メモリで収まらないほど大きいときはローカルストレージを利用します。インデックス作成も同様です。
普段容量を意識するほうがクラスタボリューム、インスタンスに紐づくのがローカルストレージです。
ローカルストレージの大きさはインスタンスサイズだけで決まります。独立して増やせないので注意。
ここでライターとリーダーの違いがあります。 一時的なデータが巨大なとき、ライターはローカルストレージだけではなくクラスターボリュームの方に書き込むことがありますが、リーダーはローカルストレージのみです。 そのため、同じ処理でライターは問題なくともリーダーではエラーになることがあります。
その他設計tips
今回は入門的な記事なため、ヒントに留め内容は割愛します。
- id、特にprimary keyは小さくする
- ページングはAPI設計から考えること 何も考えないと負荷が厳しい
- 削除フラグではなく状態(ステータス)を考える
- 毎回削除フラグをチェックしてるなら削除済みテーブルを分離してみる
- 全員にまとめて追加するのはなく、アクセスがあった時点でデータ変更する
- マスタデータにも履歴がある
- 履歴には現在ではなくそのときのマスタデータを利用する
- 日付にはシステム日付と業務日付がある
Aurora MySQLの設定変更
最近はデフォルトでも良い感じなので少しだけ紹介します。
本番でt系インスタンスを避ける
以下の理由で重要なプロダクトでは控えましょう。
- CPUのスロットリング。40%ぐらいまでしか使えません。
- バーストは短い時間だけ
- 一部機能が使えない
i/oが多いならi/o optimizedを
i/o optimizedにすることで、インスタンス単価と引き換えにi/o代を無料にできます。 また、i/oパフォーマンスが向上するメリットもあります。
https://pages.awscloud.com/rs/112-TZM-766/images/AWS-39_Database_AWS_Summit_JP_2024.pdf
i/o量はCloudWatchはもちろん、コスト面からCost Explorerでも確認できます。 Reserved Instance代は1.3倍になり、購入ユニットに小数が登場するので注意。
バックアップ
初期設定は1日です。さすがに短いので伸ばしましょう。
削除保護
設定しましょう
IAM認証を使う
MySQLにログインするなら、よりセキュアです。
ログ出力
初期設定は0なので必要に応じて出力しましょう
Serverless v2 と開発
Scale to Zeroが可能になったServerless v2ですが、RDS Proxyを入れていたら0になりません。そもそもRDS Proxy代が高めですね。
RDS proxyの制約
RDS ProxyがあるとAuroraのB/Gデプロイ機能が現在使えません
メンテナンスウィンドウ
ZDPもありつつ本番のワークロードに応じて設定する
Data API
DBコネクションではなくAPIでのアクセスです。 用途は2通りあります。
マネジメントコンソールから踏み台の代わりに使って作業する。内部統制さえ気をつければ便利です。踏み台が消せればむしろ総合的にはセキュリティが上がるかもしれません。
アプリケーションからアクセスする。特にLambdaを使ったサーバーレスアプリケーションには理想的な仕組みなのですが、残念ながらほとんどのORMが本格対応していません。今後に期待しましょう。
参考ドキュメント
結局ドキュメントの読み込み具合で差がつきます
なんだかんだで公式ドキュメントにも全部書いてあります
こちらも流し読みしましょう。パフォーマンス周りは基礎を集中的に抑えるフェーズが途中で必要になってきます。そちらの方が近道です
最後に
DynamoDBガイドに比べると量が多くなってしまいましたが それだけ気をつけないと負荷やシステム運用周りの問題が発生しやすいということです。
RDBはグローバルであり、遅いクエリのせいで無関係なクエリまで遅くなります。ペットと捉え、きめ細かいケアを続けることを覚悟しましょう。
文責: 高木