الفهارس والأداء في MongoDB — MongoDB
الفهارس والأداء في MongoDB
بلا فهرس، find({ email }) على مليون مستند يفحص المستندات كلها (COLLSCAN). الفهرس بنية مرتبة (B-Tree) تقود الاستعلام للمستندات مباشرة (IXSCAN). الفهارس هي أهم أداة أداء في أي قاعدة بيانات.
إنشاء الفهارس
// بالمشغّل الرسمي
// await db.collection("users").createIndex({ email: 1 }, { unique: true });
// بـ Mongoose داخل المخطط
// userSchema.index({ email: 1 }, { unique: true });
// orderSchema.index({ customer: 1, createdAt: -1 });
const indexSpec = { customer: 1, createdAt: -1 }; // 1 تصاعدي، -1 تنازلي
console.log(indexSpec);| النوع | الاستخدام |
|---|---|
| فردي { email: 1 } | البحث بحقل واحد |
| فريد { unique: true } | منع التكرار (البريد) — يُفرض من القاعدة لا من الكود |
| مركب { customer: 1, createdAt: -1 } | استعلامات تجمع عدة حقول وترتيباً |
| على مصفوفة (multikey) | { tags: 1 } يفهرس كل عنصر |
| TTL { expiresAt: 1 }, { expireAfterSeconds: 0 } | حذف تلقائي للمستندات المنتهية (رموز التحقق، الجلسات) |
| نصي { title: "text" } | بحث نصي أساسي (لبحث متقدم: Atlas Search) |
| جزئي partialFilterExpression | فهرسة جزء من المستندات فقط (مثل status: "active") |
قاعدة ESR لترتيب حقول الفهرس المركب
Equality ← Sort ← Range:
// الاستعلام: طلبات عميل محدد، الأحدث أولاً، بمبلغ أكبر من 100
// find({ customer: id, total: { $gt: 100 } }).sort({ createdAt: -1 })
const esrIndex = { customer: 1, createdAt: -1, total: 1 }; // مساواة ← ترتيب ← مدى
console.log(Object.keys(esrIndex).join(" ← "));وكما في SQL: الفهرس { customer: 1, createdAt: -1 } يخدم الاستعلام بـ customer وحده، لكنه لا يخدم الاستعلام بـ createdAt وحده (البادئة).
اقرأ خطة التنفيذ
// const plan = await Order.find({ customer: id }).sort({ createdAt: -1 }).explain("executionStats");
// plan.queryPlanner.winningPlan ← IXSCAN أم COLLSCAN؟
// plan.executionStats.totalDocsExamined مقابل nReturned
const healthy = { nReturned: 20, totalDocsExamined: 20 }; // قريبان = فهرس فعّال
const unhealthy = { nReturned: 20, totalDocsExamined: 480000 };
console.log(healthy.totalDocsExamined / healthy.nReturned, unhealthy.totalDocsExamined / unhealthy.nReturned);القاعدة العملية: totalDocsExamined قريب من nReturned = جيد. فارق ضخم = فهرس ناقص أو غير مناسب. وفي Atlas يقترح Performance Advisor الفهارس الناقصة من الاستعلامات البطيئة الفعلية.
الفهارس ليست مجانية
كل فهرس يبطئ الكتابة ويستهلك ذاكرة (الأفضل أن تتسع الفهارس المستخدمة في الذاكرة). احذف الفهارس غير المستخدمة ($indexStats). في الإنتاج، Mongoose يبني الفهارس تلقائياً عند البدء (autoIndex) — عطّله في الإنتاج وأنشئ الفهارس عمداً عبر سكربت أو ترحيل، لأن بناء فهرس على مجموعة ضخمة مكلف.
أنماط تصميم للأداء
- الإسقاط: اطلب الحقول التي تحتاجها (select("name price")) — أقل شبكة وذاكرة، وقد يُخدم الاستعلام من الفهرس وحده (covered query).
- الترقيم بالمؤشر (createdAt < lastSeen) بدل skip الكبير: skip(100000) يمر على 100,000 مستند.
- نمط القيمة المحسوبة: احفظ commentsCount في المنشور وحدّثه بـ $inc بدل عدّ التعليقات في كل عرض.
- نمط الدلو (bucket): لبيانات زمنية كثيفة (قراءات حساسات) — مستند لكل ساعة يضم القراءات، أو استخدم Time Series Collections.
- تجنب المصفوفات غير المحدودة في المستند (كل تعليقات منشور مشهور): تنمو حتى حد 16MB وتبطئ كل تحديث.
تمرين
لمجموعة orders بحقول (customer، status، total، createdAt)، صمم فهارس لهذه الاستعلامات، وبرر الترتيب بقاعدة ESR: طلبات عميل الأحدث أولاً؛ الطلبات المعلقة الأقدم من ساعة؛ أكبر 10 طلبات اليوم.
// indexes.js — تعريف الفهارس بوعي وفحص فعاليتها (npm install mongoose)
import mongoose from "mongoose";
const { Schema } = mongoose;
const orderSchema = new Schema(
{
customer: { type: Schema.Types.ObjectId, ref: "Customer", required: true },
status: { type: String, enum: ["pending", "paid", "shipped", "cancelled"], required: true },
total: { type: Number, required: true, min: 0 },
},
{ timestamps: true, autoIndex: process.env.NODE_ENV !== "production" } // في الإنتاج: إنشاء متعمد
);
// ESR: مساواة (customer) ← ترتيب (createdAt) ← مدى (total)
orderSchema.index({ customer: 1, createdAt: -1, total: 1 });
// فهرس جزئي: الطلبات المعلقة فقط (أصغر بكثير من فهرسة الكل)
orderSchema.index({ createdAt: 1 }, { partialFilterExpression: { status: "pending" } });
const verificationSchema = new Schema({
email: { type: String, required: true },
codeHash: { type: String, required: true },
expiresAt: { type: Date, required: true },
});
// TTL: يحذف MongoDB المستند تلقائياً بعد expiresAt
verificationSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
const Order = mongoose.models.OrderIdx ?? mongoose.model("OrderIdx", orderSchema);
const Verification = mongoose.models.Verification ?? mongoose.model("Verification", verificationSchema);
await mongoose.connect(process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/kodax_demo");
try {
await Promise.all([Order.syncIndexes(), Verification.syncIndexes()]); // تطبيق الفهارس المعرّفة فقط
console.log("الفهارس:", (await Order.listIndexes()).map((i) => i.name));
const customerId = new mongoose.Types.ObjectId();
const plan = await Order.find({ customer: customerId, total: { $gt: 100 } })
.sort({ createdAt: -1 })
.limit(20)
.explain("executionStats");
const stats = plan.executionStats;
console.log("المرحلة الفائزة:", JSON.stringify(plan.queryPlanner.winningPlan).includes("IXSCAN") ? "IXSCAN ✅" : "COLLSCAN ⚠️");
console.log("فُحص", stats.totalDocsExamined, "مستنداً لإعادة", stats.nReturned);
// ترقيم بالمؤشر بدل skip الكبير
const firstPage = await Order.find({ customer: customerId }).sort({ createdAt: -1 }).limit(20).lean();
const last = firstPage.at(-1);
if (last) {
await Order.find({ customer: customerId, createdAt: { $lt: last.createdAt } }).sort({ createdAt: -1 }).limit(20).lean();
}
} finally {
await mongoose.disconnect();
}اختبر فهمك