اعمال چندین عملیات روی یک آرایه در فریم‌ورک Aggregation

Perform Multiple Operations on An Array in the Aggregation Framework

26 اردیبهشت 1401
درسنامه درس 75 از سری دوره جامع آموزش MongoDB
MongoDB: اعمال چندین عملیات روی یک آرایه در فریم ورک Aggregation (قسمت 77)

اگر یادتان باشد در انتهای جلسه قبل به شما تکلیفی دادم که بدین شرح بود: ما می خواهیم برای هر فرد فقط بالاترین نمره کسب شده را نمایش بدهیم! یعنی دیگر آرایه examScores را نداشته باشیم بلکه بالاترین نمره مستقیما به جایش قرار بگیرد. برای انجام این کار چندین روش مختلف وجود دارد. یکی از این روش ها بدین صورت است که ابتدا برای به دست آوردن بالاترین نمره، باید کل آرایه را unwind$ کنیم تا به ازای هر نفر چندین سند بگیریم (اگر یادتان باشد در جلسات قبل این اتفاق افتاد و به ازای هر فرد چندین کپی دریافت کردیم که مقدار hobbies در آن ها فرق می کرد). سپس اسناد برگردانده شده را بر اساس فرد، sort می کنیم و سپس بالاترین نمره برای هر فرد را پیدا کرده و دریافت می کنیم.

در این جلسه می خواهیم این مسئله را با هم حل کنیم. همانطور که در خلاصه جواب گفتم، قدم اول unwind$ کردن کل examScore ها است:

db.friends.aggregate([
    { $unwind: "$examScores" }
]).pretty()

با این کار به ازای هر examScore یک کپی از سند ساخته می شود تا هر سند فقط یک examScore داشته باشد. سپس باید نتایج را به صورت نزولی Sort$ کنیم:

db.friends.aggregate([
    { $unwind: "$examScores" },
    { $sort: { examScores: -1 } }
]).pretty()

به نظر شما با اجرای کوئری بالا چه اتفاقی می افتد؟ اولین فردی که به ما برگردانده می شود به شکل زیر است:

"_id" : ObjectId("5ec48cfe35361420f36550a9"), 
"name" : "Maria",                             
"hobbies" : [                                 
        "Cooking",                            
        "Skiing"                              
],                                            
"age" : 29,                                   
"examScores" : {                              
        "difficulty" : 8,                     
        "score" : 44.2                        
}

همانطور که می بینید نمره او 44.2 است که اصلا بالا نیست. به نظرتان مشکل از کجاست؟ دقیقا! examScores یک embedded document است و ما نمی توانیم بر اساس یک شیء sort$ کنیم. آیا ما می توانیم فیلد score را از درون examScores خارج کرده و فقط آن را unwind کنیم؟

db.friends.aggregate([
    { $unwind: "$examScores.score" },
    { $sort: { examScores: -1 } }
]).pretty()

متاسفانه کوئری بالا نیز کار نمی کند چرا که examScores.score اصلا آرایه نیست بنابراین unwind$ روی آن کار نمی کند. یکی از راه حل های موجود این است که sort را روی examScores.score تنظیم کنیم:

db.friends.aggregate([
    { $unwind: "$examScores" },
    { $sort: { "examScores.score": -1 } }
]).pretty()

این کوئری به خوبی کار می کند و حالا اسنادی را که دارای نمره بالاتری باشند در اول صفحه می آورد. نکته ای را به خاطر بسپارید که نباید در قسمت sort برای examScores.score از علامت $ استفاده کنید (یعنی بگویید examScores.score$) چرا که ما به نام فیلد اشاره کرده ایم و نه به مقدار آن؛ شما زمانی از $ استفاده می کنید که به مقدار یک فیلد اشاره داشته باشید اما examScores.score در اینجا نام key می باشد. نتیجه اجرای کوئری بالا به شکل زیر است (فقط نفر اول را می آورم):

"_id" : ObjectId("5ec48cfe35361420f36550a7"), 
"name" : "Max",                               
"hobbies" : [                                 
        "Sports",                             
        "Cooking"                             
],                                            
"age" : 29,                                   
"examScores" : {                              
        "difficulty" : 3,                     
        "score" : 88.5                        
}

همانطور که مشخص است نفر اول دارای بالاترین نمره (88.5) است. با اینکه این روش کار می کند، من می خواهم از روش دیگری استفاده کنم تا stage های بیشتری داشته باشیم. ما می توانیم از یک projection نیز استفاده کنیم:

db.friends.aggregate([
    { $unwind: "$examScores" },
    { $project: { _id: 0, name: 1, age: 1, score: "$examScores.score" } },
    { $sort: { score: -1 } }
]).pretty()

بنابراین فقط name و age و score را داریم که البته score را خودم تعریف کرده ام. این بار از آنجایی که به مقدار examScores.score اشاره می کنیم حتما باید از $ استفاده کنیم. سپس در قسمت sort گفته ایم که بر اساس همین فیلد جدید score، داده ها را مرتب کن. نتیجه اجرای این کوئری به شکل زیر است:

{ "name" : "Max", "age" : 29, "score" : 88.5 }
{ "name" : "Maria", "age" : 29, "score" : 75.1 }
{ "name" : "Manu", "age" : 30, "score" : 74.3 }
{ "name" : "Max", "age" : 29, "score" : 62.1 }
{ "name" : "Maria", "age" : 29, "score" : 61.5 }
{ "name" : "Max", "age" : 29, "score" : 57.9 }
{ "name" : "Manu", "age" : 30, "score" : 53.1 }
{ "name" : "Manu", "age" : 30, "score" : 52.1 }
{ "name" : "Maria", "age" : 29, "score" : 44.2 }

حالا می بینید که داده های ما کاملا باز شده و به ترتیب نمرات مرتب شده اند. به نظر شما برای حذف کاربر های تکراری چه کار باید کرد؟ دقیقا! ما می توانیم از یک group$ استفاده کنیم:

db.friends.aggregate([
    { $unwind: "$examScores" },
    { $project: { _id: 1, name: 1, age: 1, score: "$examScores.score" } },
    { $sort: { score: -1 } },
    { $group: { _id: "$_id", name: { $first: "$name" }, maxScore: { $max: "$score" } } }
]).pretty()

در ابتدا در قسمت projection مقدار id را 1 گذاشته ام تا به آن دسترسی داشته باشیم. سپس در قسمت group$ از اپراتور first استفاده کرده ایم. این اپراتور می گوید اولین مقداری را که دیدی، انتخاب کن! از آنجایی که نام افراد تکراری در داده های ما موجود است من name$ را به آن داده ام تا اولین نامی را که دید انتخاب کند. اولین نام قطعا نام کاربری با بالاترین نمره خواهد بود (چرا که قبلا آن را sort کرده ایم). سپس فیلد maxScore را تعریف کرده ام که در آن از اپراتور max$ استفاده کرده ایم. این اپراتور بیشترین مقدار را به ما برمی گرداند. با اجرای کوئری بالا نتیجه زیر را می گیریم:

"_id" : ObjectId("5ec48cfe35361420f36550a7"), 
"name" : "Max",                               
"maxScore" : 88.5                             
                                              
                                              
"_id" : ObjectId("5ec48cfe35361420f36550a9"), 
"name" : "Maria",                             
"maxScore" : 75.1                             
                                              
                                              
"_id" : ObjectId("5ec48cfe35361420f36550a8"), 
"name" : "Manu",                              
"maxScore" : 74.3                             

بنابراین داده ها مرتب شده اند. البته ممکن است نتیجه برگردانده شده برای شما به شکل زیر باشد:

"_id" : ObjectId("5ec48cfe35361420f36550a8"), 
"name" : "Manu",                              
"maxScore" : 74.3                             
                                              
                                              
"_id" : ObjectId("5ec48cfe35361420f36550a7"), 
"name" : "Max",                               
"maxScore" : 88.5                             
                                              
                                              
"_id" : ObjectId("5ec48cfe35361420f36550a9"), 
"name" : "Maria",                             
"maxScore" : 75.1

مشخص است که ترتیب نمره ها بهم ریخته است. یعنی ما در حال نمایش افراد با بالاترین نمره هستیم اما خود این افراد بر اساس نمره مرتب نشده اند. بنابراین برای اینکه مطمئن شویم داده های ما حتما به ترتیب نمره ظاهر می شوند باید یک sort را به انتهای کوئری خود اضافه کنیم:

db.friends.aggregate([
    { $unwind: "$examScores" },
    { $project: { _id: 1, name: 1, age: 1, score: "$examScores.score" } },
    { $sort: { score: -1 } },
    { $group: { _id: "$_id", name: { $first: "$name" }, maxScore: { $max: "$score" } } },
    { $sort: { maxScore: -1 } }
]).pretty()

با اجرای این کوئری همیشه نمره های بالاتر، در ابتدای لیست نمایش داده می شوند.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش MongoDB توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

مقالات مرتبط
آخرین سوالات کاربران
5451218 در 3 سال قبل پرسیده:
ما را دنبال کنید
اینستاگرام روکسو تلگرام روکسو ایمیل و خبرنامه روکسو