DB パフォーマンス改善 Phase 1 — 場当たり対応から計測ファースト運用への転換
1,424,125 行の受注テーブルを含む tasteck DB に 7 本のインデックスを追加。ゲストカレンダー・着信履歴一覧・ポイント残高計算などの主要クエリを EXPLAIN で range scan 化し、本番無停止で適用しました。同時に、Shopify / GitHub / Notion / Figma の DB 進化論文を読み直し、これまでの『症状駆動の場当たり対応』を卒業して『計測ファースト』運用に切り替える判断をした背景を共有します。
tasteck のデータベース改善が一段落しました。本記事では (1) 本日本番適用した 7 本のインデックス追加 の内容、(2) その過程で直面した「場当たり対応の罠」と、業界事例・論文を参照して切り替えた 計測ファースト方針、そして (3) ついでにリリースした 顧客モバイルのポイント履歴画面 を、運用者と開発者の両方に向けて解説します。
何をやったか — Phase 1 の中身
tasteck の本番 MySQL (8.0.45) に対し、以下 7 本の複合インデックスを追加しました。
| テーブル | インデックス | 狙い |
|---|---|---|
call_histories | (company_id, created_at) | 着信履歴一覧の降順取得 |
call_histories | (company_id, guest_id) | ゲスト別着信の検索 |
guest_notes | (company_id, cast_name_id, updated_at) | キャスト別ゲストノート |
guest_notes | (company_id, guest_id) | ゲスト別ゲストノート |
guest_points | (company_id, guest_id, shop_id) | ポイント残高計算 |
orders | (cast_name_id, status, plan_in_time) | キャスト別受注一覧 |
orders | (shop_id, status, plan_in_time) | 顧客カレンダーの店舗絞り込み |
orders テーブルは 1,424,125 行 あり index 作成にもそれなりの処理が必要ですが、MySQL 8.0 の ALGORITHM=INPLACE により 本番無停止 で適用できました。実際の本番適用時間は 5 本合計で 70 秒。
適用後の EXPLAIN で主要クエリが期待通り range scan + 新 index に流れていることを確認しました。特に影響が大きかったのは顧客モバイルのカレンダー画面で、従来は shop_id 単独 FK index しか使えず filesort が発生していたところ、(shop_id, status, plan_in_time) 複合 index により index condition pushdown + ソート済みインデックスで処理が完結するようになりました。
「場当たり対応」から抜け出せなかった半年
正直に書きます。この 7 本の index を入れるまで、tasteck は 症状駆動の部分最適 を繰り返してきました。
流れはいつも同じでした。
- ユーザーから「○○画面が遅い」の報告が来る
- 該当 API を特定し、SQL を読む
- その 1 クエリに合わせて index を足す
- 次の遅い画面が来たら、また別の index を足す
これを続けると、起こることは決まっています。
- 見えてる遅さしか直せない。ユーザーが声を上げた箇所だけが改善される
- 全体最適ができない。そのとき限りの index 追加なので、他の似たケースはカバーされない
- 終わりがない。1 つ直すと、隠れていた別の遅さが見えてくる
半年間、この構造から抜けられずにいました。
論文と業界事例を読み直した
今回、全 7 本の index を一気に追加する判断をした背景には、業界事例と学術論文の読み直しがあります。具体的には以下を参照しました。
- Shopify Pods Architecture — 2014 年まで単一 MySQL で粘った
- Partitioning GitHub's relational databases to handle scale — 10 年以上
mysql1単一クラスタ - Herding elephants: lessons learned from sharding Postgres at Notion — 2020 年までに 480 論理 / 32 物理にシャード
- How Figma's Databases Team Lived to Tell the Scale — 2022 年まで単一 Postgres + Replica で粘る
- Guide to scaling your database — PlanetScale — sharding 検討ラインは working set 500GB–1TB
- Brent Ozar "How Many Indexes Are Too Many?" — 1 テーブル 8 index 以下目安、write/read 10× 超は削除候補
- Markus Winand "Use The Index, Luke" — leftmost prefix が最重要、選択性先頭は myth
そこから見えた tasteck の現在地がこれです。
tasteck の規模 (~100 社、orders 130 万行、単一 RDS + 単一 EC2) は、Shopify が 2010 年、GitHub が 2015 年、Notion が 2018 年頃に居た地点 に近い。
つまり、単一 MySQL でまだ数年は戦える段階。これを認識したうえで、やるべきことの順序が明確になりました。
1. 計測基盤の導入 ← 最優先・まだやれていない
2. Index 健康診断 ← Phase 1 で前払い
3. Read Replica 活用 ← 次にコスパが高い
4. 様子見 → 必要なら垂直分割 or sharding (2 年後以降)
一番下の sharding は、Shopify・Notion・Figma 全員が「もっと遅くやるべきだった」と振り返っている 領域。tasteck の規模で手を出すのは明確な過剰投資です。一方、Read Replica 導入は全社が「もっと早くやればよかった」と語る。この温度差を念頭に優先順位を組み直しました。
Phase 1 は「前払い」と位置付ける
理想論で言えば、計測基盤 (Phase 0) を先に作り、fingerprint 上位 20 クエリを特定してから index を追加すべきです。
しかし現状、どのクエリが遅いかは半年の運用で十分判明していました。そこで今回は 「計測基盤なしに index を足すのはこれで最後」 という自己ルールを置いたうえで、既知の主要遅クエリへの対処として 7 本を先行適用しました。これは正当化できる前払いだと判断しています。
Brent Ozar 基準 (1 テーブル 8 index 以下) もクリアしており、追加した index が実データで使われていることは EXPLAIN で確認済み。使われていないことが判明したら sys.schema_unused_indexes 経由で削除可能です。
Phase 0 計測基盤に着手 — 今日から QA で稼働
同日、QA の RDS に カスタム Parameter Group を作成し、以下を適用しました。
slow_query_log = 1
long_query_time = 0.5
log_output = TABLE # mysql.slow_log テーブルに直接書き込み
log_output = TABLE がポイントで、これにより SELECT * FROM mysql.slow_log で即時 SQL 解析が可能。pt-query-digest のようにログファイルをダウンロードする手間がありません。本番 RDS にも近日中に同じ設定を適用予定です (60-120 秒の reboot が必要なため深夜帯で実施)。
そのうえで、fingerprint 集計スクリプト (pt-query-digest の軽量版) を書きました。このスクリプトは数字・引用符・IN 句を正規化し、同一パターンの SQL を集約して Total Time / Avg Time / Rows Examined で降順ソートします。月次でこのレポートを見れば「tasteck で今一番重いクエリ Top 20」が常に見えるようになります。
この Phase 0 が整うまで、今後の index 追加は原則禁止 というルールにしました。これが場当たり対応の罠から抜ける一番確実な方法だと考えています。
Phase 2 以降の展望
Phase 1 (index) と Phase 0 (計測) が揃った次のステップは Read Replica の導入です。
- RDS Read Replica を 1 台追加 (+ 月 3-5 万円程度)
- 分析 EC2 (
prd-notel-analytics) の DB 接続先を Replica に切り替え - 顧客モバイルの read-only 系 (カレンダー・ポイント残高) も Replica 向きに
これだけで Primary の read 負荷が 40-60% 下がる想定です。分析系クエリが最も重いため、そこを物理的に分離するインパクトが大きい。Shopify・Notion・Figma がシャーディング前に通った道でもあり、tasteck 規模では 最もコスパの高い施策 と位置付けています。
逆にやらないこと、現時点で 2 年封印しているのが次の 3 つです。
- シャーディング — working set < 500GB では過剰投資、確実に後悔する
- CQRS フル実装 / Event Sourcing — 100 社規模では運用負荷が benefit を上回る
- MySQL 外への移行 — Percona Toolkit / Vitess エコシステムに残る方が将来選択肢が多い
おまけ: ポイント履歴画面を追加しました
技術的な話ばかりになりましたが、本日はユーザー体験側にも一つアップデートがあります。顧客モバイル (guest.no-tel.com) の ポイント画面にポイント付与/使用履歴を追加 しました。
従来、ポイント画面には合計残高と店舗別残高しか表示されていませんでした。どうやってそのポイントになったのかが見えず、「いつ何で付与された?」「この使用はどの予約?」が確認できない状態でした。
新しい画面 (/point/history) は、利用履歴ページと同じ操作パターンで設計しています。
- 日付降順で一覧表示 (付与は緑
+100 pt、使用は赤-500 ptで一目瞭然) - 店舗フィルタ (セレクトボックス、複数店舗利用のゲスト向け)
- 20 件ずつの「もっと見る」方式でページネーション
- スタッフの運用メモ (
remarks) は内部用なので非表示
UI の配置はドロップダウンではなく別ページ遷移を選んでいます。店舗数が増えた場合の一覧性、モバイルでの OS ネイティブピッカーの使い勝手を優先したためです。
バックエンドには履歴取得用に 3 本のエンドポイントを新設しました。
GET /api/guest/guestPoint/findAllHistory/:email?limit&offset&shopId
GET /api/guest/guestPoint/countHistory/:email?shopId
GET /api/guest/guestPoint/findHistoryShops/:email
この API は今回追加した IDX_guest_point_company_guest_shop インデックスを活用するため、レスポンスも高速です。
まとめ
- Phase 1 完了: 本番 orders に 7 本の index を無停止適用、主要クエリが range scan 化
- 方針転換: 論文 + 業界事例で tasteck の現在地を再評価、計測ファースト運用へ
- Phase 0 開始: slow_query_log + fingerprint 集計スクリプトを QA で稼働中
- 次の一手: 本番 slow_log 展開 → Read Replica 検討 → 2 年は sharding 封印
- ユーザー向け: 顧客モバイルにポイント付与/使用履歴画面を追加
tasteck は「業界特化マイクロ SaaS」というポジショニング上、派手な技術選択よりも「地味な計測と健全化」の積み上げが武器になると考えています。これからもこの温度感でやっていきます。