یه اسکریپت Import رو اجرا میکنید و انتظار دارید چند هزار رکورد در چند ثانیه وارد دیتابیس بشن، ولی ترمینال همین‌طور جلو میره، CPU و دیتابیس درگیرن و ذخیره اطلاعات بیشتر از چیزی که انتظار داشتید طول میکشه. عجیب‌تر اینکه کد هم خیلی ساده‌ست؛ یه حلقه دارید و داخلش برای هر Object یه save() اجرا میکنید.

مشکل دقیقاً همینجاست.

وقتی برای هر رکورد جداگانه save() میزنید، Django معمولاً برای هر Object یه عملیات جدا به دیتابیس میفرسته. یعنی اگه ۱۰ هزار رکورد داشته باشید، ممکنه با هزاران عملیات مجزا طرف باشید؛ در حالی که خیلی از اون‌ها رو میشه به شکل گروهی و با رفت‌وآمد خیلی کمتر بین برنامه و دیتابیس ذخیره کرد.

برای همین Django قابلیتی برای ساخت چندین رکورد به صورت یکجا داره که با اسم bulk_create() شناخته میشه.

مشکل save داخل Loop چیه؟

خود save() هیچ ایرادی نداره و برای ذخیره یه Object یا تعداد کمی رکورد کاملاً طبیعیه. مشکل وقتی شروع میشه که اون رو داخل یه حلقه بزرگ قرار بدیم.

هر بار که برنامه یه Query جدید به دیتابیس میفرسته، فقط زمان اجرای دستور مطرح نیست؛ ارتباط بین برنامه و دیتابیس، پردازش دستور، مدیریت Transaction و برگردوندن نتیجه هم هزینه داره.

حالا این هزینه رو چند هزار بار تکرار کنید.

ممکنه خود هر Query خیلی سریع باشه، ولی مجموع هزاران Query کوچیک تبدیل به یه عملیات کند بشه. این موضوع موقع وارد کردن فایل CSV، انتقال داده، ساخت اطلاعات اولیه یا پردازش تعداد زیادی رکورد بیشتر خودش رو نشون میده.

bulk_create چه کاری متفاوت انجام میده؟

ایده خیلی ساده‌ست: به جای اینکه رکوردها رو یکی‌یکی به دیتابیس تحویل بدیم، اول Objectها رو در Python آماده میکنیم و بعد یه مجموعه از اون‌ها رو برای ذخیره گروهی ارسال میکنیم.

مثلاً:

users = [
    User(
        username=f"user_{i}",
        email=f"user_{i}@example.com"
    )
    for i in range(10_000)
]

User.objects.bulk_create(
    users,
    batch_size=1000
)

اینجا ۱۰ هزار Object ساخته شده، ولی قرار نیست برای تک‌تک اون‌ها یه save() جدا اجرا کنیم. Django اون‌ها رو به صورت گروهی وارد دیتابیس میکنه و با batch_size هم مشخص کردیم هر دسته حداکثر هزار رکورد داشته باشه.

نتیجه معمولاً تعداد رفت‌وآمد خیلی کمتر به دیتابیس و زمان اجرای بهتره.

batch_size برای چیه؟

وقتی میگیم «اندازه هر دسته از رکوردها»، منظور همون batch_size هست.

شاید وسوسه‌کننده باشه که هر ۱۰۰ هزار رکورد رو یکجا بفرستیم، ولی Query خیلی بزرگ هم میتونه دردسر خودش رو داشته باشه. بعضی دیتابیس‌ها روی تعداد پارامترها یا اندازه دستور محدودیت دارن و Queryهای خیلی بزرگ میتونن مصرف حافظه بیشتری ایجاد کنن.

با batch_size میتونید عملیات رو به چند بخش منطقی تقسیم کنید. مثلاً هر بار ۵۰۰ یا ۱۰۰۰ رکورد ذخیره بشه.

عدد مناسب به دیتابیس، حجم هر رکورد و سرور شما بستگی داره و یه عدد جادویی برای همه پروژه‌ها وجود نداره.

سرعت بیشتر رایگانه؟ نه دقیقاً!

bulk_create() سریع‌تره چون بخشی از مسیر معمول ذخیره هر Object رو دور میزنه و دقیقاً همینجا باید حواسمون جمع باشه.

وقتی از save() معمولی استفاده میکنید، ممکنه منطق سفارشی داخل متد save() مدل داشته باشید. همچنین بعضی رفتارهایی که هنگام ذخیره معمولی انتظار دارید، در ذخیره گروهی به همون شکل اجرا نمیشن.

یعنی نباید فقط به خاطر اینکه bulk_create() سریع‌تره، هر save() داخل پروژه رو با اون عوض کنید.

این ابزار بیشتر زمانی عالیه که رکوردهای زیادی دارید و عملیات ذخیره اون‌ها ساده و قابل پیش‌بینیه.

Signalها رو فراموش نکنید

در Django میشه قبل یا بعد از ذخیره یه مدل، کدی به صورت خودکار اجرا کرد. به این سازوکار پیام‌های داخلی مربوط به رویدادهای مدل گفته میشه که با اسم Signal شناخته میشن.

وقتی از bulk_create() استفاده میکنید، نباید انتظار داشته باشید رفتار ذخیره تک‌تک Objectها دقیقاً مثل اجرای جداگانه save() باشه. پس اگه منطق مهمی به Signalهای ذخیره یا متد save() وابسته دارید، قبل از مهاجرت به ذخیره گروهی حتماً اون بخش رو بررسی کنید.

گاهی یه برنامه با bulk_create() خیلی سریع‌تر میشه ولی یه منطق جانبی مهم دیگه اجرا نمیشه و بعداً باگش خودش رو نشون میده.

کجا bulk_create واقعاً می‌درخشه؟

یکی از بهترین جاها وارد کردن داده از فایل‌های بزرگه. مثلاً فایل CSV با ۵۰ هزار محصول دارید، اطلاعات رو میخونید، Objectها رو میسازید و بعد در چند دسته وارد دیتابیس میکنید.

ساخت داده آزمایشی، انتقال اطلاعات بین دو سیستم، پردازش داده‌های جمع‌آوری‌شده و ساخت رکوردهای اولیه هم نمونه‌های خوب دیگه هستن.

هرجا تعداد رکوردها بالاست و لازم نیست برای تک‌تک اون‌ها منطق پیچیده ذخیره اجرا بشه، bulk_create() ارزش بررسی داره.

پس همیشه ازش استفاده کنیم؟

نه. برای ساخت یه کاربر یا یه Order عادی در View، استفاده از save() یا create() کاملاً منطقیه. سود اصلی bulk_create() وقتی مشخص میشه که تعداد عملیات بالا میره.

همچنین قبل از بهینه‌سازی باید اندازه‌گیری کنید. اگه فقط ۲۰ رکورد دارید، احتمالاً تفاوت اونقدر نیست که پیچیدگی اضافی ارزشش رو داشته باشه.

اما وقتی حلقه‌ای دیدید که چند هزار بار داره save() اجرا میکنه، اونجا یه چراغ باید توی ذهنتون روشن بشه.

حواستون به حافظه هم باشه

ذخیره گروهی قرار نیست تمام مشکلات Performance رو به شکل جادویی حل کنه.

اگه برای وارد کردن ۵ میلیون رکورد اول هر ۵ میلیون Object رو داخل یه List بزرگ قرار بدید، ممکنه این بار دیتابیس سریع باشه ولی RAM برنامه تحت فشار قرار بگیره.

برای داده‌های خیلی بزرگ بهتره خود پردازش ورودی هم دسته‌بندی بشه؛ یعنی یه بخش از داده رو بخونید، Objectها رو بسازید، ذخیره کنید و بعد برید سراغ بخش بعدی.

بهینه‌سازی واقعی فقط کم کردن Query نیست؛ باید کل مسیر داده رو ببینید.

جمع بندی

وقتی تعداد زیادی رکورد دارید، اجرای save() داخل یه حلقه میتونه هزاران عملیات جدا برای دیتابیس ایجاد کنه و زمان اجرای کار رو بالا ببره. bulk_create() کمک میکنه چندین Object رو به شکل گروهی ذخیره کنید و تعداد رفت‌وآمد بین Django و دیتابیس رو کاهش بدید.

برای Import فایل، ساخت داده زیاد و پردازش‌های گروهی، این روش میتونه تفاوت خیلی بزرگی ایجاد کنه. فقط قبل از استفاده بررسی کنید که منطق مهمی داخل save() یا Signalهای مدل نداشته باشید و برای حجم‌های خیلی بزرگ هم از دسته‌بندی مناسب استفاده کنید.

اگه یه Loop دارید که هزاران بار save() صدا میزنه، قبل از ارتقای سرور یه بار هم به bulk_create() نگاه کنید؛ شاید مشکل سخت‌افزار نیست، فقط دارید با دیتابیس خیلی بیشتر از چیزی که لازمه حرف میزنید!