11. MongoDB: Индексы
Индексы в MongoDB работают аналогично индексам в реляционных БД — ускоряют поиск, но замедляют запись. MongoDB поддерживает различные типы индексов для разных сценариев.
Типы индексов
Заголовок раздела «Типы индексов»graph TD A[MongoDB Indexes] --> B[Single Field] A --> C[Compound] A --> D[Multikey - массивы] A --> E[Text] A --> F[Geospatial] A --> G[Hashed] A --> H[Wildcard]Single Field Index
Заголовок раздела «Single Field Index»// Создание индексаdb.users.createIndex({ email: 1 }) // 1 = ascending, -1 = descending
// Уникальный индексdb.users.createIndex({ username: 1 }, { unique: true })
// Частичный индекс (только для активных пользователей)db.users.createIndex( { email: 1 }, { partialFilterExpression: { status: "active" } })
// Sparse индекс (только документы с полем)db.users.createIndex( { phoneNumber: 1 }, { sparse: true })Compound Index
Заголовок раздела «Compound Index»// Составной индекс (порядок важен!)db.orders.createIndex({ status: 1, createdAt: -1 })
// Эффективно для:db.orders.find({ status: "pending" }).sort({ createdAt: -1 })db.orders.find({ status: "completed", createdAt: { $gte: date } })
// Неэффективно (обратный порядок):db.orders.find({ createdAt: { $gte: date } }) // не использует индекс полностьюПравило: Индекс {a: 1, b: 1, c: 1} поддерживает:
{a}{a, b}{a, b, c}
Но НЕ поддерживает: {b}, {c}, {b, c}
Multikey Index (для массивов)
Заголовок раздела «Multikey Index (для массивов)»// Документ:// { _id: 1, tags: ["mongodb", "database", "nosql"] }
// Индекс на массивdb.articles.createIndex({ tags: 1 })
// Эффективно работает с:db.articles.find({ tags: "mongodb" })db.articles.find({ tags: { $in: ["mongodb", "redis"] } })
// ⚠️ Нельзя создать compound multikey index на два массива!// db.posts.createIndex({ tags: 1, categories: 1 }) // ERROR если оба массивыText Index
Заголовок раздела «Text Index»// Создание text индексаdb.articles.createIndex({ title: "text", content: "text" })
// Веса для полей (title важнее content)db.articles.createIndex( { title: "text", content: "text" }, { weights: { title: 10, content: 5 } })
// Поискdb.articles.find({ $text: { $search: "mongodb tutorial" } })
// Поиск с фразойdb.articles.find({ $text: { $search: "\"full text search\"" } })
// Исключение словdb.articles.find({ $text: { $search: "mongodb -SQL" } })
// Сортировка по релевантностиdb.articles.find( { $text: { $search: "mongodb" } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } })⚠️ Ограничение: Только один text индекс на коллекцию!
Geospatial Index
Заголовок раздела «Geospatial Index»// 2dsphere индекс для GeoJSONdb.places.createIndex({ location: "2dsphere" })
// Документ:db.places.insertOne({ name: "Central Park", location: { type: "Point", coordinates: [-73.968285, 40.785091] // [longitude, latitude] }})
// Поиск в радиусе (в метрах)db.places.find({ location: { $near: { $geometry: { type: "Point", coordinates: [-73.9, 40.7] }, $maxDistance: 5000 // 5km } }})
// Поиск в полигонеdb.places.find({ location: { $geoWithin: { $geometry: { type: "Polygon", coordinates: [[ [-74.0, 40.7], [-73.9, 40.7], [-73.9, 40.8], [-74.0, 40.8], [-74.0, 40.7] ]] } } }})Hashed Index
Заголовок раздела «Hashed Index»Используется для sharding.
// Hashed индексdb.users.createIndex({ userId: "hashed" })
// Хорош для равномерного распределения в sharding// Не поддерживает диапазонные запросы ($gt, $lt)Wildcard Index
Заголовок раздела «Wildcard Index»Для динамических схем и вложенных полей.
// Индекс на все поля в metadatadb.products.createIndex({ "metadata.$**": 1 })
// Теперь работает для любых полей:db.products.find({ "metadata.color": "red" })db.products.find({ "metadata.size": "large" })db.products.find({ "metadata.brand.name": "Apple" })
// Wildcard на всю коллекциюdb.products.createIndex({ "$**": 1 })TTL Index (Time-To-Live)
Заголовок раздела «TTL Index (Time-To-Live)»Автоматическое удаление старых документов.
// Удалять документы через 1 час после createdAtdb.sessions.createIndex( { createdAt: 1 }, { expireAfterSeconds: 3600 })
// Удалять в конкретное время (expireAt)db.events.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
// Документ:db.events.insertOne({ message: "Event", expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // через 24 часа})⚠️ Важно: TTL background задача запускается каждые 60 секунд!
Управление индексами
Заголовок раздела «Управление индексами»// Список всех индексовdb.users.getIndexes()
// Удаление индексаdb.users.dropIndex("email_1")db.users.dropIndex({ email: 1 })
// Удаление всех индексов (кроме _id)db.users.dropIndexes()
// Пересоздание индекса (background)db.users.createIndex( { email: 1 }, { background: true } // не блокирует запись)
// Информация о размере индексовdb.users.stats().indexSizesАнализ использования индексов
Заголовок раздела «Анализ использования индексов»// Explain - анализ запроса
// Проверка использования индексаconsole.log(explain.executionStats.totalDocsExamined) // должно быть минимальноconsole.log(explain.executionStats.executionTimeMillis)
// Index hints (принудительное использование)Хорошие показатели:
totalDocsExamined≈nReturnedexecutionStage: "IXSCAN"(использует индекс)executionTimeMillis<100ms
Плохие показатели:
executionStage: "COLLSCAN"(сканирование всей коллекции!)totalDocsExamined>>nReturned
TypeScript примеры
Заголовок раздела «TypeScript примеры»import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');const db = client.db('myapp');
// Создание индексов при инициализацииasync function setupIndexes() { const users = db.collection('users');
// Уникальные индексы await users.createIndex({ email: 1 }, { unique: true }); await users.createIndex({ username: 1 }, { unique: true });
// Compound индексы await users.createIndex({ status: 1, createdAt: -1 });
// Частичный индекс await users.createIndex( { lastLoginAt: 1 }, { partialFilterExpression: { status: 'active' }, expireAfterSeconds: 90 * 24 * 60 * 60 // 90 дней } );
console.log('Indexes created');}
// Анализ производительности запросаasync function analyzeQuery(collection: string, query: any) { const result = await db.collection(collection) .find(query) .explain('executionStats');
const stats = result.executionStats;
return { executionTime: stats.executionTimeMillis, docsExamined: stats.totalDocsExamined, docsReturned: stats.nReturned, usedIndex: stats.executionStages.stage === 'IXSCAN', indexName: stats.executionStages.indexName || null, efficiency: stats.totalDocsExamined === 0 ? 100 : (stats.nReturned / stats.totalDocsExamined * 100).toFixed(2) };}
// Пример использованияasync function main() { await client.connect(); await setupIndexes();
// Анализ запроса console.log('Query analysis:', analysis);
// Slow queries мониторинг const slowQueries = await findSlowQueries(); console.log('Slow queries:', slowQueries);
await client.close();}
// Мониторинг медленных запросовasync function findSlowQueries(thresholdMs: number = 100) { // Включить profiling await db.command({ profile: 2, slowms: thresholdMs });
// Через некоторое время проверить system.profile const slowQueries = await db.collection('system.profile') .find({ millis: { $gte: thresholdMs } }) .sort({ ts: -1 }) .limit(10) .toArray();
return slowQueries.map(q => ({ operation: q.op, query: q.command, duration: q.millis, timestamp: q.ts }));}
// Статистика по индексамasync function getIndexStats(collectionName: string) { const stats = await db.collection(collectionName).aggregate([ { $indexStats: {} } ]).toArray();
return stats.map(stat => ({ name: stat.name, operations: stat.accesses.ops, since: stat.accesses.since }));}Mongoose индексы
Заголовок раздела «Mongoose индексы»import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true, // создаёт уникальный индекс lowercase: true, trim: true }, email: { type: String, required: true, unique: true, index: true // обычный индекс }, status: { type: String, enum: ['active', 'inactive', 'banned'] }, createdAt: { type: Date, default: Date.now }, lastLoginAt: Date, metadata: mongoose.Schema.Types.Mixed});
// Compound индексыuserSchema.index({ status: 1, createdAt: -1 });
// Text индексuserSchema.index({ username: 'text', email: 'text' });
// GeospatialuserSchema.index({ location: '2dsphere' });
// Частичный индексuserSchema.index( { lastLoginAt: 1 }, { partialFilterExpression: { status: 'active' }, expireAfterSeconds: 90 * 24 * 60 * 60 });
// Wildcard индексuserSchema.index({ 'metadata.$**': 1 });
const User = mongoose.model('User', userSchema);
// Автоматическое создание индексов при стартеawait User.createIndexes();💡 Best Practices
Заголовок раздела «💡 Best Practices»-
ESR Rule для compound индексов:
- Equality (
=) - Sort
- Range (
<,>,>=,<=)
Пример:
{ status: 1, createdAt: -1 }дляfind({ status: "active" }).sort({ createdAt: -1 }) - Equality (
-
Покрывающие индексы:
db.users.createIndex({ email: 1, username: 1, status: 1 })// Не обращается к документам, читает только из индекса! -
Не создавайте лишние индексы:
- Каждый индекс замедляет INSERT/UPDATE
- Занимает дополнительное место
- Только на часто используемых полях
-
Background индексы для production:
db.users.createIndex({ email: 1 }, { background: true }) -
Мониторинг:
- Используйте MongoDB Atlas или Ops Manager
- Анализируйте slow query log
- Проверяйте
$indexStatsaggregation
Index Intersection
Заголовок раздела «Index Intersection»MongoDB может использовать несколько индексов одновременно:
// Индексы:db.users.createIndex({ status: 1 })db.users.createIndex({ age: 1 })
// Запрос может использовать оба:db.users.find({ status: "active", age: { $gte: 18 } })
// Но лучше создать compound:db.users.createIndex({ status: 1, age: 1 })⚠️ Частые ошибки
Заголовок раздела «⚠️ Частые ошибки»- Создание индексов в неправильном порядке (compound)
- Слишком много индексов (замедляют запись)
- Забывают добавить
background: trueна production - Не анализируют
explain()для медленных запросов - Используют
$regexбез префикса (не использует индекс)
Следующий урок: Sharding в MongoDB →