در این مقاله، به بررسی دقیق این پدیده در دیتابیس و برنامهنویسی، دلایل وقوع و راههای فرار از آن میپردازیم.
در ریاضیات، حاصلضرب دکارتی دو مجموعه $A$ و $B$، مجموعهای است که شامل تمام جفتهای ممکن از اعضای این دو مجموعه است.
فرمول ساده آن به این صورت است:
$|A \times B| = |A| \times |B|$
حالا تصور کنید در دنیای نرمافزار، به جای دو مجموعه کوچک، با جداول دیتابیسی سروکار داریم که هزاران رکورد دارند. وقتی دو یا چند مجموعه داده را به شکلی اشتباه یا بدون فیلتر به هم متصل (Join) میکنیم، تعداد ردیفهای خروجی به صورت نمایی رشد میکند. این رشد ناگهانی و عظیم دادهها را انفجار دکارتی مینامند.
این اتفاق زمانی رخ میدهد که شما دو جدول را بدون مشخص کردن رابطه (Join Condition) به هم متصل کنید یا از CROSS JOIN استفاده کنید.
الف) مثال کلاسیک SQL
فرض کنید دو جدول داریم:
جدول Users: شامل ۱,۰۰۰ کاربر.
جدول Products: شامل ۱,۰۰۰ محصول.
اگر بنویسید:
SQL
SELECT * FROM Users, Products;
دیتابیس سعی میکند هر کاربر را با هر محصول ترکیب کند. نتیجه؟ ۱,۰۰۰,۰۰۰ ردیف! در حالی که شاید شما فقط میخواستید بدانید هر کاربر چه محصولی را خریده است. این حجم از داده باعث مصرف شدید RAM، درگیر شدن CPU و اشغال پهنای باند شبکه میشود.
ب) تلهی روابط یکبهچند (One-to-Many)
خطرناکترین نوع انفجار دکارتی زمانی است که شما چندین رابطه «یکبهچند» را در یک Query واحد Load میکنید.
مثال:
فرض کنید یک موجودیت Blog دارید که:
هر بلاگ ۱۰ عدد Comment دارد.
هر بلاگ ۱۰ عدد Tag دارد.
اگر بخواهید یک بلاگ را همراه با کامنتها و تگهایش در یک دستور SQL بگیرید:
SQL
SELECT * FROM Blogs
JOIN Comments ON Blogs.Id = Comments.BlogId
JOIN Tags ON Blogs.Id = Tags.BlogId
WHERE Blogs.Id = 1;
شاید فکر کنید کلا ۲۰ ردیف برمیگردد، اما دیتابیس حاصلضرب اینها را برمیگرداند: $1 \times 10 \times 10 = 100$ ردیف.
حالا اگر بلاگ ۱۰۰ کامنت و ۱۰۰ تگ داشت، شما ۱۰,۰۰۰ ردیف دریافت میکردید، در حالی که مجموع دادههای واقعی فقط ۲۰۱ ردیف است (۱ بلاگ + ۱۰۰ کامنت + ۱۰۰ تگ). در این حالت، اطلاعات بلاگ در هر ۱۰ هزار ردیف تکرار (Duplicate) میشود!
در دنیای داتنت، نسخههای قدیمی EF Core (قبل از ۳.۰) این مشکل را با اجرای چندین کوئری جداگانه حل میکردند. اما از نسخه ۳ به بعد، برای بهبود عملکرد، EF تصمیم گرفت همه چیز را در یک کوئری SQL بیاورد (Single Query). این کار باعث شد توسعهدهندگان ناگهان با انفجار دکارتی مواجه شوند.
مثال عملی در #C
var blog = context.Blogs
.Include(b => b.Comments)
.Include(b => b.Tags)
.FirstOrDefault(b => b.Id = 1);
اگر تعداد کامنتها و تگها زیاد باشد، این کد میتواند به شدت کند شود و حافظه سرور را ببلعد.
در کدنویسی معمولی، این انفجار معمولاً در حلقههای تو در تو (Nested Loops) رخ میدهد.
foreach (var user in users) // 1,000 users
{
foreach (var product in products) // 1,000 products
{
// انجام یک عملیات
// این بلاک کد 1,000,000 بار اجرا میشود!
}
}
اگر پیچیدگی الگوریتم شما $O(N \times M)$ یا بدتر از آن $O(N^2)$ باشد، با بزرگ شدن دیتا، برنامه شما عملاً متوقف میشود.
۱. استفاده از Split Queries (در ORMها)
در EF Core 5.0 و بالاتر، راهکار رسمی استفاده از متد .AsSplitQuery() است. این متد به جای یک Join غولآسا، چند Query مجزا به دیتابیس میزند:
var blog = context.Blogs
.Include(b => b.Comments)
.Include(b => b.Tags)
.AsSplitQuery() // نجاتدهنده!
.FirstOrDefault(b => b.Id = 1);
۲. استفاده از DTO و Projection
به جای کشیدن کل موجودیتها با تمام جزییات، فقط فیلدهایی که نیاز دارید را Select کنید:
var data = context.Blogs
.Select(b => new {
Title = b.Title,
CommentCount = b.Comments.Count(),
Tags = b.Tags.Select(t => t.Name).ToList()
}).ToList();
۳. بهینهسازی حلقهها (استفاده از HashMaps/Dictionaries)
۴. فیلتر کردن زودهنگام (Filtering)
همیشه در SQL یا LINQ، ابتدا دادهها را با Where محدود کنید و سپس Join یا Include بزنید.
۵. Pagination (صفحهبندی)
فرض کنید میخواهید گزارشی بگیرید از "لیست فاکتورها به همراه تمامی اقلام فاکتور و تمامی تراکنشهای بانکی مرتبط با آن فاکتور".
تعداد فاکتورهای مورد نظر: ۱۰۰
میانگین اقلام هر فاکتور (Items): ۵۰
میانگین تراکنشهای هر فاکتور (Transactions): ۵ (مثلاً تراکنشهای ناموفق و موفق)
بدون Split Query:
تعداد ردیفهای خروجی دیتابیس: $100 \times 50 \times 5 = 25,000$ ردیف سنگین.
با Split Query یا کوئری جداگانه:
کوئری اول: ۱۰۰ ردیف (فاکتورها)
کوئری دوم: ۵,۰۰۰ ردیف (اقلام)
کوئری سوم: ۵۰۰ ردیف (تراکنشها)
مجموع ردیفهای پردازش شده: ۵,۶۰۰ ردیف.
تفاوت بین ۵,۶۰۰ و ۲۵,۰۰۰ ردیف، تفاوت بین یک گزارش آنی و گزارشی است که باعث Time-out شدن سایت میشود.
انفجار دکارتی زمانی رخ میدهد که ما بدون توجه به ساختار روابط، مجموعههای داده را در هم ضرب میکنیم.
در دیتابیس، با استفاده از Joinهای صحیح و تکنیک Split Query از آن جلوگیری کنید.
در کدنویسی، با پرهیز از حلقههای تو در تو غیرضروری و استفاده از ساختمان دادههای سریع (مثل Dictionary) آن را مهار کنید.
همیشه به یاد داشته باشید: دیتابیس شما سطل زباله نیست! فقط چیزی را بخواهید که واقعاً نیاز دارید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.