در جلسه قبل فایلی حاوی 5 هزار document را به شما دادم و از شما خواستم که آن را با دستور mongoimport وارد پایگاه داده و کالکشن جدیدی به نام contactData و contacts کنید. برای تست این موضوع می توانید دستور show dbs را اجرا کرده تا تمام پایگاه های داده خود را مشاهده نمایید. من در لیست پایگاه های داده، contactData را می بینم بنابراین دستور ما با موفقیت اجرا شده و اطلاعات فایل را وارد کرده است. در قدم اول باید ببینیم هر contact ما یا هر کدام از Document های ما چه ساختاری دارند. برای این کار ابتدا به پایگاه داده متصل می شویم:
use contactData
سپس با یک دستور ساده findOne یکی از این سند ها را نمایش می دهیم:
db.contacts.findOne()
با اجرای دستور بالا ساختار زیر برای ما برگردانده می شود:
{ "_id": ObjectId("5eb8ff52abf8f8026baa90a1"), "gender": "male", "name": { "title": "mr", "first": "victor", "last": "pedersen" }, "location": { "street": "2156 stenbjergvej", "city": "billum", "state": "nordjylland", "postcode": 56649, "coordinates": { "latitude": "-29.8113", "longitude": "-31.0208" }, "timezone": { "offset": "+5:30", "description": "Bombay, Calcutta, Madras, New Delhi" } }, "email": "victor.pedersen@example.com", "login": { "uuid": "fbb3c298-2cea-4415-84d1-74233525c325", "username": "smallbutterfly536", "password": "down", "salt": "iW5QrgwW", "md5": "3cc8b8a4d69321a408cd46174e163594", "sha1": "681c0353b34fae08422686eea190e1c09472fc1f", "sha256": "eb5251e929c56dfd19fc597123ed6ec2d0130a2c3c1bf8fc9c2ff8f29830a3b7" }, "dob": { "date": "1959-02-19T23:56:23Z", "age": 59 }, "registered": { "date": "2004-07-07T22:37:39Z", "age": 14 }, "phone": "23138213", "cell": "30393606", "id": { "name": "CPR", "value": "506102-2208" }, "picture": { "large": "https://randomuser.me/api/portraits/men/23.jpg", "medium": "https://randomuser.me/api/portraits/med/men/23.jpg", "thumbnail": "https://randomuser.me/api/portraits/thumb/men/23.jpg" }, "nat": "DK" }
همانطور که می بینید هر فرد موارد زیر را دارد:
برای شروع یک کوئری ساده می نویسیم تا تمام افراد بالای 60 سال را پیدا کنیم:
db.contacts.find({"dob.age": {$gt: 60}}).pretty()
همچنین اگر می خواهیم بدانیم، چند نفر بالای 60 سال دارند باید به جای pretty از count استفاده کنیم:
db.contacts.find({"dob.age": {$gt: 60}}).count()
نتیجه این کوئری عدد 1222 است. یعنی 1222 نفر بالای 60 سال هستند. حالا باید یک کوئری را با index اجرا کنیم تا تفاوت را حس کنیم اما از آنجایی که همه چیز را local (روی سیستم خود) اجرا می کنیم و تعداد document های ما هم زیاد نیست (5 هزار سند اصلا زیاد نیست) نمی توانیم تفاوتی را ببینیم چرا که این تفاوت ها در حد میلی ثانیه است. خوشبختانه MongoDB ابزاری را به ما داده است تا با استفاده از آن ببینیم هر کوئری به چه شکلی اجرا شده است. به کوئری زیر نگاه کنید:
db.contacts.explain().find({"dob.age": {$gt: 60}})
متد explain برای تمام دستوراتی کار می کند که در آن ها به دنبال یک document هستیم بنابراین Find و update و غیره با آن کار می کنند اما insert کار نمی کند. با اجرای کوئری بالا نتیجه زیر را می گیریم:
"queryPlanner" : { "plannerVersion" : 1, "namespace" : "contactData.contacts", "indexFilterSet" : false, "parsedQuery" : { "dob.age" : { "$gt" : 60 } }, "queryHash" : "FC9E47D2", "planCacheKey" : "FC9E47D2", "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "dob.age" : { "$gt" : 60 } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "Amir-PC", "port" : 27017, "version" : "4.2.5", "gitVersion" : "2261279b51ea13df08ae708ff278f0679c59dc32" }, "ok" : 1
این یک گزارش کامل از جریان اجرای کوئری ما است. MongoDB بر اساس plan (به معنی «نقشه») کار می کند. plan ها راه های مختلف اجرای یک کوئری هستند (گزینه های مختلفی که MongoDB برای اجرای کوئری دارد) و در نهایت یکی از آن ها انتخاب شده و اجرا می شود که به آن winningPlan (یعنی نقشه برنده) می گوییم. شما می توانید winningPlan را در پاسخ بالا پیدا کنید. اگر به دقت به این کد نگاه کنید خصوصیت stage را می بینید که روی COLLSCAN است که همان collection scan ای است که در جلسه قبل در مورد آن صحبت کردیم. همچنین خصوصیتی به نام rejectedPlans (به معنی نقشه های رد شده) را داریم که در مثال ما خالی است. چرا؟ به دلیل اینکه ما index نداریم بنابراین تنها راه یا تنها plan ممکن، استفاده از collection scan است و راه های مختلفی نداریم که بخواهیم یکی یا چند تا از آن ها را رد (reject) کنیم.
ما با پاس دادن آرگومان های خاصی به explain می توانیم گزارش کامل تری را دریافت کنیم. مثلا:
db.contacts.explain("executionStats").find({"dob.age": {$gt: 60}})
executionStats (به معنی آمار اجرای کوئری) گزارش کاملی از آمار جزئی اجرای این کوئری دریافت خواهید کرد که به شکل زیر است:
"queryPlanner" : { "plannerVersion" : 1, "namespace" : "contactData.contacts", "indexFilterSet" : false, "parsedQuery" : { "dob.age" : { "$gt" : 60 } }, "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "dob.age" : { "$gt" : 60 } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "executionStats" : { "executionSuccess" : true, "nReturned" : 1222, "executionTimeMillis" : 4, "totalKeysExamined" : 0, "totalDocsExamined" : 5000, "executionStages" : { "stage" : "COLLSCAN", "filter" : { "dob.age" : { "$gt" : 60 } }, "nReturned" : 1222, "executionTimeMillisEstimate" : 0, "works" : 5002, "advanced" : 1222, "needTime" : 3779, "needYield" : 0, "saveState" : 39, "restoreState" : 39, "isEOF" : 1, "direction" : "forward", "docsExamined" : 5000 } }, "serverInfo" : { "host" : "Amir-PC", "port" : 27017, "version" : "4.2.5", "gitVersion" : "2261279b51ea13df08ae708ff278f0679c59dc32" }, "ok" : 1
همانطور که می بینید قسمت executionStats نیز در انتهای گزارش قبلی ما ضمیمه شده است. در این گزارش خصوصیتی به نام executionTimeMillis را داریم که عدد 4 را جلوی خودش دارد (در سیستم شما و با توجه به منابع CPU شما ممکن است عدد متفاوتی داشته باشیم) که یعنی 4 میلی ثانیه طول کشیده است تا این کوئری به طور کامل اجرا شود. همچنین خصوصیت دیگری به نام "totalDocsExamined" را نیز داریم که جلوی آن عدد 5000 قرار گرفته است و به ما می گوید برای اجرای این کوئری، چند سند (document) بررسی و چک شده اند. یعنی برای پیدا کردن 1222 نفر (مشخص شده در خصوصیت "nReturned" که تعداد نتایج برگردانده شده را نشان می دهد) باید 5 هزار document (کل کالکشن) را اسکن کنیم. خود این مسئله (تفاوت شدید بین 1222 تا 5000) به ما نشان می دهد که این کوئری آنقدر ها هم بهینه نیست.
بیایید یک index را برای آن تعریف کنیم و ببینیم که نتیجه چطور تغییر می کند. برای این کار باید از دستور createIndex استفاده کنیم که اولین آرگومان آن، نام فیلدی است که باید برایش index تعریف شود. شما می توانید از فیلد های مستقیم یا فیلد های embedded مانند dob.age استفاده کنید. آرگومان دوم به صورت یک مقدار برای آرگومان اول پاس داده می شود و می تواند یکی از مقادیر زیر باشد:
آیا واقعا این مقدار مهم است؟ صادقانه بگویم اصلا اهمیتی ندارد و به همین دلیل هم هست که اکثر برنامه نویسان عدد 1 را می گذارند و از این مرحله رد می شوند. بنابراین کوئری نهایی ما بدین شکل خواهد بود:
db.contacts.createIndex({"dob.age": 1})
با اجرای کوئری بالا نتیجه زیر برای ما نمایش داده می شود:
"createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1
یعنی هیچ خطایی نداشته ایم. numIndexesBefore مشخص می کند که قبل از ساخت این index چند index داشته ایم که 1 بوده است. حتما می پرسید ما که از قبل index ای نداشتیم! در جلسات بعد برایتان توضیح خواهم داد که چرا چنین مقداری نمایش داده می شود اما فعلا آن را نادیده بگیرید. numIndexesAfter یعنی پس از ایجاد index جدید چند index داریم که 2 عدد است. حالا من دوباره کوئری زیر را اجرا می کنم:
db.contacts.explain("executionStats").find({"dob.age": {$gt: 60}})
حالا از بین مقادیر برگردانده شده "executionTimeMillis" برای من 1 میلی ثانیه کمتر شده است. ممکن است برای شما 2 یا 3 یا 4 میلی ثانیه کمتر شود. شاید این عدد در ظاهر کم باشد اما برای پایگاه های داده بزرگ بسیار خوب است، پایگاه داده ما کوچک است بنابراین شاهد جهش شدیدی نخواهیم بود. همچنین می بینید که دو stage مختلف را در کد های برگردانده شده می بینیم (من فقط قسمت executionStats را می آورم):
"executionStats" : { "executionSuccess" : true, "nReturned" : 1222, "executionTimeMillis" : 3, "totalKeysExamined" : 1222, "totalDocsExamined" : 1222, "executionStages" : { "stage" : "FETCH", "nReturned" : 1222, "executionTimeMillisEstimate" : 0, "works" : 1223, "advanced" : 1222, "needTime" : 0, "needYield" : 0, "saveState" : 9, "restoreState" : 9, "isEOF" : 1, "docsExamined" : 1222, "alreadyHasObj" : 0, "inputStage" : { "stage" : "IXSCAN", "nReturned" : 1222, "executionTimeMillisEstimate" : 0, "works" : 1223, "advanced" : 1222, "needTime" : 0, "needYield" : 0, "saveState" : 9, "restoreState" : 9, "isEOF" : 1, "keyPattern" : { "dob.age" : 1 }, "indexName" : "dob.age_1", "isMultiKey" : false, "multiKeyPaths" : { "dob.age" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "dob.age" : [ "(60.0, inf.0]" ] }, "keysExamined" : 1222, "seeks" : 1, "dupsTested" : 0, "dupsDropped" : 0 } } }, "serverInfo" : { "host" : "Amir-PC", "port" : 27017, "version" : "4.2.5", "gitVersion" : "2261279b51ea13df08ae708ff278f0679c59dc32" }, "ok" : 1
همانطور که می بینید، stage یا مرحله اول IXSCAN (مخفف index scan) است. بنابراین collection scan نداشتیم. در همان قسمت nReturned روی 1222 قرار گرفته است. این یعنی 1222 کلید یا pointer به محل دقیق سند ها در کالکشن برگردانده شده است. این 1222 تعداد سند های برگردانده شده نیست بلکه محل سند هایی است که باید برگردانده شوند. stage بعدی ما FETCH است (در گزارش بالا مشاهده می کنید) که در آن مقدار nReturned روی 1222 قرار گرفته است. اینجا 1222 یعنی تعداد سند هایی که برگردانده شده است. در نهایت در قسمت اولیه گزارش خودتان که من در بالا نیاورده ام، می بینید که "totalKeysExamined" (تعداد key های بررسی شده) دیگر 5000 نیست بلکه همان 1222 key موجود در لیست ایندکس ما است.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.