エラーの内容
重要なアップデートをプッシュしているとき、あるいはデータベースのマイグレーションを実行しているときに、CI/CDパイプラインが突然停止することがあります。ログには、データベースからの次のような拒絶メッセージが表示されています。
MongoServerError: add index fails, too many indexes for ns: mydb.mycollection
これはバグや一時的な不具合ではありません。コレクションあたり64個というインデックスのハードリミットに達したのです。これは、データベースの動作が極端に遅くなるのを防ぐために、WiredTigerストレージエンジンに組み込まれている保護用の制限です。
発生の原因
64個のインデックスというのは十分な数に聞こえるかもしれませんが、複雑なアプリケーションでは予想以上に早くこの壁に突き当たることがよくあります。インデックスの追加にはコストが伴います。MongoDBは、insert、update、またはdeleteが行われるたびに関連するすべてのB-treeを更新する必要があるため、インデックスを1つ追加するごとに書き込み操作に約10〜20%のオーバーヘッドが発生します。
主な原因には以下のようなものがあります。
- ORMによる過剰なインデックス作成: MongooseやTypeORMなどのツールが、スキーマで定義されたすべてのフィールドに対してインデックスを自動作成している可能性があります。
- レガシーな負債: 古い機能は削除されても、それをサポートしていたインデックスがアクティブなまま残り、リソースを消費し続けている。
- 「クエリごとにインデックス」という習慣: 複合構造を使用する代わりに、
find()クエリのあらゆる組み合わせに対して個別のユニークインデックスを作成している。
コレクションに1億個のドキュメントが含まれている場合、冗長なインデックスが1つあるだけで、ワーキングセットに切実に必要なRAMを5GBも浪費している可能性があります。
即時の解決策:監査とクリーンアップ
この制限を設定で増やすことはできません。先に進むための唯一の方法は、不要なものを削除して空きを作ることです。
ステップ1:現在のインデックスを把握する
mongoshを開き、現在コレクションでアクティブなすべてのインデックスをリストアップします。
db.mycollection.getIndexes()
keyの定義を注意深く確認してください。重複しているものがないか探します。
ステップ2:冗長なプレフィックスを排除する
MongoDBのインデックスは左から右へと機能します。複合インデックスがある場合、「プレフィックス」(最初の1つまたは複数のフィールド)を使用するクエリはすでにカバーされています。次の2つのインデックスを考えてみましょう。
- インデックス A:
{ "tenant_id": 1 } - インデックス B:
{ "tenant_id": 1, "created_at": -1 }
インデックスAは完全に冗長です。インデックスBは、インデックスAが担当していたあらゆるクエリを処理できます。インデックスAを削除すれば、クエリのパフォーマンスに影響を与えることなく、即座にスロットが解放され、書き込みのレイテンシが減少します。
ステップ3:不要なインデックスを削除する
冗長なインデックスが見つかったら、その名前を指定して削除します。
db.mycollection.dropIndex("tenant_id_1")
残りのインデックス数をカウントして、進捗を確認します。
db.mycollection.getIndexes().length
長期的な解決戦略
アプリケーションがどうしても60個以上のデータアクセスパターンを必要とする場合は、インデックス戦略を変更する必要があります。
1. 複合インデックスによる統合
適切に設計された{ shop_id: 1, status: 1, category: 1 }のような1つの複合インデックスは、3つの異なるクエリタイプをサポートできます。これは、shop_id単体での検索、shop_id + status、および3つすべてのフィールドを組み合わせた検索をカバーします。インデックスの汎用性を高めるために、この「ESR」(Equality, Sort, Range:等価、ソート、範囲)ルールを活用してください。
2. ワイルドカードインデックスへの切り替え
何十ものカスタムユーザーフィールドや動的メタデータにインデックスを作成していませんか?それをやめて、代わりにワイルドカードインデックスを使用してください。
db.mycollection.createIndex({ "user_metadata.$**": 1 })
これはuser_metadataオブジェクト内のすべてのネストされたフィールドをカバーします。最大の利点は、追跡するサブフィールドの数に関係なく、64個のインデックス制限に対してちょうど1つとしてカウントされることです。
3. $indexStatsで未使用のインデックスを特定する
どのインデックスが有用かを推測しないでください。$indexStats集計を使用して、最後の再起動以降の実際の使用状況を確認します。
db.mycollection.aggregate([ { $indexStats: {} } ])
accesses.opsフィールドを確認してください。本番環境のトラフィックが1週間経過しても操作(ops)が0のままであれば、そのインデックスは安全に削除できます。
確認
クリーンアップ後、新しいインデックスの作成を再試行してください。今度は成功するはずです。
db.mycollection.createIndex({ "new_feature_key": 1 }, { name: "idx_feature_v1" })
// 次のように返されるはずです: { "ok": 1 }
よく使われるクエリに対してdb.mycollection.explain("executionStats").find(...)を実行し、古いインデックスの削除によってコレクションスキャンなどのパフォーマンス低下が発生していないことを確認してください。
予防チェックリスト
- 自動インデックス作成を無効にする: Mongooseでは
autoIndex: falseを設定します。インデックスはマイグレーションスクリプト経由で管理し、本番環境で予期せぬ事態が起こらないようにします。 - 制限を監視する: MongoDB AtlasやDatadogで、コレクションのインデックス数が50に達したときにトリガーされるアラートを設定します。
- TTLを見直す: TTL(Time To Live)インデックスは便利ですが、制限にカウントされます。控えめに使用してください。
- 四半期ごとに監査を行う: 定期的なタスクとして
$indexStatsを実行し、未使用のインデックスを整理するスケジュールを組みます。

