NoSQL و MongoDB
دیتابیسهای NoSQL داده را به شکلی نگه میدارند که برنامه میخواند، نه در جدولهای نرمال. در MongoDB هر رکورد یک سند کامل است. طراحی را از کوئریها شروع کن و فقط وقتی NoSQL را انتخاب کن که شکل داده یا مقیاس واقعاً آن را لازم دارد.
نویسنده: bezzad
مشکل: محصولهایی با شکلهای مختلف
فروشگاه ما گوشی، پیراهن و کتاب میفروشد. هر دسته ویژگیهای خودش را دارد:
- گوشی: حافظه، اندازه صفحه، رنگ.
- پیراهن: سایز، جنس پارچه.
- کتاب: نویسنده، تعداد صفحه.
در یک دیتابیس رابطهای (SQL) دو راه ساده داریم و هر دو مشکل دارند:
- یک جدول با صدها ستون. بیشتر ستونها برای هر محصول خالی میمانند و با هر دسته جدید، جدول عوض میشود.
- جدول «ویژگی و مقدار». هر ویژگی یک ردیف است. ساختن یک صفحه محصول چند JOIN لازم دارد و کوئریها پیچیده میشوند.
در یک دیتابیس سندی، هر محصول یک سند است با همان ویژگیهایی که لازم دارد. سند گوشی و سند پیراهن لازم نیست یک شکل داشته باشند.
خانوادههای NoSQL
کلمه NoSQL یک نوع دیتابیس نیست. چند خانواده مختلف است که فقط در «جدول رابطهای نبودن» مشترکاند:
در مصاحبه بیشتر از همه درباره دیتابیس سندی (مثل MongoDB) میپرسند. بقیه این درس درباره همان است.
مدل سند: جاسازی یا اشاره؟
در SQL، اول داده را نرمال میکنی و بعد هر کوئری را با JOIN میسازی. در MongoDB برعکس است. اول میپرسی برنامه چه چیزی را با هم میخواند، بعد سند را به همان شکل میسازی.
برای سفارش، دو انتخاب داریم:
- جاسازی (Embed). ردیفهای سفارش داخل خود سند سفارش هستند.
- اشاره (Reference). سند جدا است و فقط شناسهاش نگه داشته میشود، مثل کلید خارجی.
کی جاسازی کنیم؟
- وقتی همیشه با هم خوانده میشوند. ردیفهای سفارش بدون سفارش معنی ندارند.
- وقتی تعدادشان محدود است. یک سفارش چند ردیف دارد، نه یک میلیون.
- وقتی با هم عوض میشوند. تغییر یک سند در MongoDB اتمی است. پس سفارش و ردیفهایش با هم و بدون تراکنش جدا ذخیره میشوند.
کی اشاره کنیم؟
- وقتی بیانتها رشد میکند. همه سفارشهای یک مشتری را داخل سند مشتری نگذار. هر سند در MongoDB حداکثر ۱۶ مگابایت است.
- وقتی جدا خوانده یا جدا عوض میشود. مشتری صفحه پروفایل خودش را دارد.
- وقتی داده مشترک است و بین چند سند تکرار میشود.
کد با MongoDB.Driver
کلاس محصول، با ویژگیهای متغیر در یک Dictionary:
public sealed class Product
{
public ObjectId Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
[BsonRepresentation(BsonType.Decimal128)] public decimal Price { get; set; }
public Dictionary<string, string> Specs { get; set; } = new();
}
ساختن اتصال، نوشتن و خواندن:
var client = new MongoClient(builder.Configuration.GetConnectionString("Mongo"));
var products = client.GetDatabase("shop").GetCollection<Product>("products");
await products.InsertOneAsync(new Product
{
Name = "Phone X",
Category = "phone",
Price = 499m,
Specs = new() { ["ram"] = "8GB", ["screen"] = "6.1 inch" }
}, cancellationToken: ct);
var cheapPhones = await products
.Find(p => p.Category == "phone" && p.Price < 600m)
.SortBy(p => p.Price)
.ToListAsync(ct);
مثل SQL، اینجا هم بدون ایندکس، MongoDB همه سندها را میخواند. برای کوئری بالا یک ایندکس ترکیبی بساز:
await products.Indexes.CreateOneAsync(
new CreateIndexModel<Product>(Builders<Product>.IndexKeys
.Ascending(p => p.Category)
.Ascending(p => p.Price)),
cancellationToken: ct);
کلاس MongoClient امن برای چند Thread است و اتصالها را خودش مدیریت میکند. پس آن را یک بار بساز و Singleton ثبت کن.
مقیاس: Sharding
یکی از دلیلهای انتخاب NoSQL، پخش داده روی چند سرور است. در MongoDB به این Sharding میگویند. هر سند بر اساس یک کلید پخش (Shard Key) به یکی از سرورها میرود.
انتخاب کلید پخش خیلی مهم است:
- کوئریهای اصلی باید کلید پخش را داشته باشند. وگرنه MongoDB باید از همه سرورها بپرسد.
- مقدارها باید خوب پخش شوند. اگر کلید پخش تاریخ ساخت باشد، همه سفارشهای جدید به یک سرور میروند و آن سرور داغ میشود.
- عوض کردنش بعداً سخت است. پس از اول با دقت انتخاب کن.
تراکنش و سازگاری
- تغییر یک سند همیشه اتمی است. اگر سند را درست طراحی کنی، بیشتر کارها فقط یک سند را عوض میکنند.
- تراکنش چندسندی هم وجود دارد، ولی گرانتر است. اگر برای هر کار ساده به آن نیاز داری، شاید مدل سند تو غلط است، یا داده واقعاً رابطهای است.
- خواندن از کپیها ممکن است کهنه باشد. اگر از Secondary بخوانی، ممکن است آخرین نوشتن را نبینی. مثل Read Replica در SQL.
انتخاب بین SQL و NoSQL
NoSQL مناسب است
- شکل داده بین رکوردها خیلی فرق دارد، مثل کاتالوگ محصول.
- برنامه همیشه یک «چیز کامل» را با هم میخواند و مینویسد.
- حجم یا سرعت نوشتن از یک سرور بیشتر است و پخش داده لازم است.
- کوئریها از قبل معلوم و محدودند.
SQL بهتر است
- داده پر از رابطه است و کوئریهای گزارشی متنوع داری.
- تراکنش بین چند رکورد کار روزمره است، مثل حسابداری.
- تیم هنوز نمیداند کوئریها چه شکلی خواهند بود.
- فقط «بدون Schema» جذاب به نظر میرسد. در عمل Schema از دیتابیس به کد منتقل میشود.
اشتباههای رایج
| اشتباه | نتیجه | راه درست |
|---|---|---|
| کپی کردن طراحی جدولهای SQL در MongoDB | کوئریهای زیاد و ترکیب داده در برنامه | طراحی سند بر اساس کوئریها |
| جاسازی لیستی که بیانتها رشد میکند | سند بزرگ و رسیدن به سقف اندازه سند | اشاره با شناسه |
| کوئری بدون ایندکس | خواندن همه سندها | ایندکس برای کوئریهای اصلی |
| کلید پخش با مقدار افزایشی، مثل تاریخ | همه نوشتنها روی یک سرور | کلید با پخش خوب |
| تراکنش چندسندی برای هر کار | کندی و پیچیدگی | مدل سندی که یک سند را عوض کند |
| انتخاب NoSQL فقط چون «بدون Schema» است | داده ناهماهنگ و باگ در کد | ستون JSON در SQL یا مدل سند سنجیده |
خلاصه در پنج خط
- کلمه NoSQL چند خانواده است: کلید و مقدار، سند، ستون گسترده و گراف.
- در MongoDB طراحی از کوئریها شروع میشود: چیزی که با هم خوانده میشود، با هم بماند.
- لیست محدود را جاسازی کن، لیست بیانتها را با شناسه اشاره کن.
- تغییر یک سند اتمی است. ایندکس و کلید پخش را با دقت انتخاب کن.
- وقتی داده پر از رابطه و تراکنش است، SQL معمولاً انتخاب بهتری است.