このエントリは「第35回 中国地方DB勉強会 in 岡山」で「LLM時代のパフォーマンスチューニング:MongoDB運用で試したコンテキスト活用の工夫」というテーマで登壇した内容をブログ化したものです。発表スライドと発表時の文字起こしをベースに、読み物として違和感のないように整え直しました。図版はスライドのものを流用しています。
背景と目的
- AIコーディングエージェントを“機能開発”だけでなく“性能改善”にも活用した実践記を共有します。
- コードだけでは見えない現実(スロークエリやアクセス分布)を「コンテキスト」として渡し、提案→実装→再測定のループで改善幅を可視化します。
- 今回の事例は MongoDB + Node/Express + Mongoose のミニECですが、RDB環境でも同じ発想を応用できます。
題材アプリの概要

- サーバーは Node.js + Express、ODM は Mongoose、データベースは Docker 上の MongoDB を利用しています。
- フロントは商品一覧と詳細のみを持つ簡易構成で、本記事ではバックエンド寄りの視点に集中します。
- プロダクトスキーマの主要フィールドは
title, description, category, price, stock, tags, createdAt, popularityです。


パフォーマンス計測設計
- データは 10 万件のサンプルを事前に投入してから計測
- 負荷は k6 の
ramping-vusを用い、同時接続 5→25 を 3 分でランプさせたあと 2 分維持し、30 秒でクールダウンさせる - リクエスト配分は一覧が 7 割、詳細が 3 割で、ソートは popularity 40%、price 30%、createdAt 30% に振り分ける。昇順と降順は半々、ページは 1〜5、件数は 12/24/48 をランダムに選択し、適宜 keyword や category、min/maxPrice を付与
- 計測対象は API レイテンシ(平均値、95 パーセンタイル)と、100ms 以上の MongoDB スロークエリ
k6シナリオは以下のような設定です(抜粋)。
export const options = { scenarios: { baseline: { executor: 'ramping-vus', startVUs: 5, stages: [ { duration: '3m', target: 25 }, { duration: '2m', target: 25 } ], gracefulRampDown: '30s', }, }, thresholds: { 'http_req_duration{endpoint:GET /products}': ['p(95)<500'], 'http_req_duration{endpoint:GET /products/:id}': ['p(95)<300'], }, }; // 7:3で一覧と詳細を叩き、ソート・ページ・条件を都度ランダム化
下記は負荷試験のアーキテクチャ図です。

改善前の状況(ベースライン)
/productsの平均は 57.90ms、p95 は 149ms(n=16,150)/products/:idの平均は 5.85ms、p95 は 35ms(n=6,951)- スロークエリは
findとcountDocumentsが中心で、p95 はそれぞれ 194ms と 179ms

API の p95 レイテンシーは次の図のとおりです。

MongoDB のスロークエリ散布図は以下のとおりです。

スロークエリログの収集と集計フロー
- サーバー側では Mongoose の自作プラグイン(slowQueryLogger)を挿入し、100ms を超えたクエリを NDJSON に 1 行追記するようにしました。
- サンプル(整形した slow query log)は次のとおりです。
{ "ts": "2025-09-12T15:48:45.906Z", "layer": "mongoose", "op": "countDocuments", "model": "Product", "collection": "products", "ms": 100, "filter": { "$or": [ { "title": {} }, { "description": {} } ] }, "options": {} }
Slow Query Log をリポジトリに commit して、Codex で slow query log を集計させました。

jq で数値を ? に正規化し、(op, model, filter, options) 単位でグルーピングして件数と p95 を CSV に出力しました。

集計CSV(抜粋):
"op","model","collection","filter","options","n","p95_ms"
"countDocuments","Product","products","{}","{}",134,143
"find","Product","products","{}","{""sort"":{""createdAt"":""?""},""skip"":""?"",""limit"":""?""}",70,151
"find","Product","products","{}","{""sort"":{""popularity"":""?""},""skip"":""?"",""limit"":""?""}",91,148
"find","Product","products","{}","{""sort"":{""price"":""?""},""skip"":""?"",""limit"":""?""}",70,150
"countDocuments","Product","products","{""$or"":[{""title"":{}},{""description"":{}}]}","{}",707,188
"find","Product","products","{""$or"":[{""title"":{}},{""description"":{}}]}","{""sort"":{""popularity"":""?""},""skip"":""?"",""limit"":""?""}",355,193
エージェントへのコンテキストと提案
Codexにはコードベースと集計した CSV をコンテキストとして渡しました。

下記のように提案してきました。

提案の核:
title + descriptionのテキストインデックスを追加popularityとcreatedAtに単一インデックスを追加price+popularityとprice+createdatの複合インデックスを追加- こちらはそのまま採用せず、まずは単一インデックスで効果を確認する。
下記は分析結果をもとに Codex が提案してきた推奨タスクです。

実装ダイジェスト
テキストインデックス(title, description)の追加
Codex のタスク画面は次のとおりです。

実際に作成した Pull Request のスクリーンショットです。
正規表現での検索から全文検索への query 修正もやってくれています。

単一インデックス(popularity, createdAt, price)の追加
こちらも Codex のタスク画面です。

対応する Pull Request のスクリーンショットです。

追加後のインデックス一覧(_id, category 以外で 4 つ追加)です。

再計測の結果
/productsの平均は 29.05ms、p95 は 141ms(n=17,812)/products/:idの平均は 0.896ms、p95 は 2ms(n=7,454)
API平均
一覧取得の平均が約半減しており、ソート軸(popularity / createdAt)の単一インデックスで走査が軽くなった影響が大きいです。

API p95
詳細取得は p95 が 35ms → 2ms と顕著に改善しており、find のスロークエリが消えたことが寄与しています。
一覧の p95 が伸び切らない要因は countDocuments で、ページングのためのカウントがボトルネック化していると考えられます。

slow query
MongoDB のスロークエリは find がほぼ消失し、countDocuments が残存しています。

まとめ
- コード外の情報(slow query やアクセスログなど)を渡すと、性能改善の精度が一気に上がります。
- ログはリポジトリにコミットする必要はなく、都度適切な形で共有・要約できれば十分に活用できます。
- 実務では Observability ツールや計測基盤から得た指標を組み合わせることで、ボトルネックの特定と検証をさらに効率化できると思います。
- 値はマスキングしても問題ありません。どんなクエリ項目の組み合わせでリクエストされるかという構造だけでも有用なコンテキストになります。
- 提案は鵜呑みにせず、変更は小さく入れてメトリクスと再測定で自分の目で確認・検証し、効果が曖昧ならロールバックも検討しましょう。