وقتی یک پروژه کند میشود، اولین پیشنهادها معمولاً ارتقای سرور یا اضافهکردن Redis هستند. گاهی مفیدند، اما بدون اندازهگیری فقط مسئله را عقب میاندازند. برای رفع کندی Laravel باید بدانیم زمان دقیقاً کجا مصرف میشود: PHP، دیتابیس، شبکه، serialization یا یک API بیرونی.
یک درخواست واقعی را اندازه بگیرید
از endpoint یا صفحهای شروع کنید که کاربر واقعاً کندی آن را حس میکند. زمان کل، تعداد query، کندترین query، حافظه و تماسهای بیرونی را ثبت کنید. داده production را بدون اطلاعات حساس نمونهبرداری کنید؛ رفتار دیتابیس با هزار رکورد با ده میلیون رکورد یکسان نیست.
Laravel Telescope برای محیط کنترلشده، profiler و slow query log دیتابیس دید خوبی میدهند. هدف جمعکردن ابزار نیست؛ ساختن یک baseline است تا بعد از تغییر بدانیم واقعاً بهتر شدهایم.
N+1 را در رابطهها پیدا کنید
نمایش ۵۰ سفارش و خواندن کاربر هر سفارش میتواند ۵۱ query بسازد. eager loading مشکل را حل میکند، به شرط اینکه فقط ستون و رابطه لازم را بگیریم:
$orders = Order::query()
->with(['customer:id,name'])
->select(['id', 'customer_id', 'total', 'status'])
->latest()
->paginate(30);
در محیط توسعه میتوانید lazy loading ناخواسته را ممنوع کنید تا مشکل پیش از production دیده شود. البته eager load کردن همه رابطهها هم راهحل نیست؛ payload و حافظه میتواند به گلوگاه بعدی تبدیل شود.
EXPLAIN را به حدس ترجیح دهید
برای query کند، SQL واقعی را با bindingها بردارید و EXPLAIN را بررسی کنید. تعداد ردیف تخمینی، نوع دسترسی و index انتخابشده مهماند. index مرکب باید با ترتیب شرطها و sort هماهنگ باشد. دو index جدا روی status و created_at لزوماً جای (status, created_at) را نمیگیرند.
استفاده از function روی ستون، تبدیل نوع و LIKE "%term" میتواند index را بیاثر کند. قبل از اضافهکردن index جدید، indexهای تکراری و هزینه write را هم ببینید.
کمتر بخوانید و کمتر تبدیل کنید
- بهجای
get()نامحدود از pagination یا chunk استفاده کنید. - فقط ستونهای لازم را select کنید.
- برای وجود رکورد از
exists()بهجای دریافت model استفاده کنید. - محاسبه aggregate را در SQL انجام دهید، نه با collection بزرگ PHP.
- responseهای API را از رابطهها و accessorهای گران پاک کنید.
کار سنگین را از مسیر پاسخ خارج کنید
ساخت فایل Excel، ارسال اعلان و پردازش تصویر لازم نیست کاربر را منتظر نگه دارد. Queue تجربه کاربری را بهتر میکند، ولی job باید timeout، retry و idempotency روشن داشته باشد. اگر یک گزارش چند دقیقهای دیتابیس اصلی را قفل میکند، انتقال به صف بهتنهایی فشار را کم نمیکند؛ query و زمان اجرای آن هم باید اصلاح شوند.
کش را هدفمند اضافه کنید
بعد از اصلاح query، داده پرخوانش و کمتغییر کاندید کش است. زمان پاسخ قبل و بعد، hit rate و فشار دیتابیس را بسنجید. کلید باید version و context مثل tenant یا زبان را در نظر بگیرد. برای داده حساس به تازگی، event تغییر میتواند cache را حذف کند.
در راهنمای Redis در Laravel درباره TTL، lock و cache stampede بیشتر توضیح دادهام.
بهینهسازی را با عدد تمام کنید
پس از هر تغییر همان سناریو را با داده مشابه اجرا کنید. میانگین بهتنهایی کافی نیست؛ p95 یا p99 نشان میدهد کاربران کندتر چه تجربهای دارند. مصرف CPU، حافظه، تعداد query و خطا را هم مقایسه کنید تا بهبود یک بخش هزینه را به جای دیگری منتقل نکرده باشد.
اگر قبل و بعد عددی نداریم، بهینهسازی بیشتر یک احساس است تا نتیجه فنی.
برای پروژهای که چند گلوگاه همزمان دارد، ممیزی فنی و کارایی Laravel میتواند گزارش اولویتبندیشده و برنامه اصلاح بسازد.
نمونه قابل تکرار: فهرست سفارشهای پرداختشده
برای جدا کردن حدس از مشاهده، یک جدول آزمایشی با ساختار مشخص بسازید. این نمونه ادعایی درباره دیتابیس یا ترافیک سایت شما ندارد؛ هدف آن نشان دادن روش مقایسه است. داده آزمایشی باید توزیع واقعینما داشته باشد: چند وضعیت مختلف، تاریخهای پراکنده و تعداد کافی سفارش برای آنکه اسکن جدول از lookup ایندکس قابل تشخیص شود. تعداد ردیفها، نسخه MySQL و سختافزار را کنار هر نتیجه ثبت کنید.
CREATE TABLE orders (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT UNSIGNED NOT NULL,
status VARCHAR(20) NOT NULL,
total DECIMAL(12,2) NOT NULL,
created_at DATETIME NOT NULL
);
CREATE INDEX orders_status_created_id
ON orders (status, created_at, id);
در Laravel، کوئری فهرست را با ستونهای موردنیاز و ترتیب قطعی بنویسید. اگر دو سفارش زمان یکسان داشته باشند، ستون id ترتیب را پایدار نگه میدارد. مقدار paid فقط مثال است؛ نام وضعیت باید با مدل داده خودتان سازگار باشد.
$orders = Order::query()
->where('status', 'paid')
->orderByDesc('created_at')
->orderByDesc('id')
->select(['id', 'customer_id', 'total', 'created_at'])
->limit(30)
->get();
SQL متناظر را با toSql() و مقدار bindingها را با getBindings() بردارید. پیش از اجرای EXPLAIN جای placeholderها را با همان مقدارها پر کنید، یا از ابزار دیتابیس که prepared statement را پشتیبانی میکند استفاده کنید. سپس EXPLAIN را یک بار بدون ایندکس پیشنهادی و بار دیگر پس از ایجاد آن اجرا کنید. ستونهای key، type، rows و Extra را ثبت کنید. خروجی نمونه را با خروجی واقعی خودتان جایگزین کنید؛ صرف انتخابشدن ایندکس به معنی سریعتر شدن قطعی نیست.
روش ساخت داده و اندازهگیری منصفانه
برای آزمایش محلی، با یک seeder چند دسته سفارش بسازید و نسبت هر وضعیت را مشخص کنید؛ مثلاً وضعیت پرتکرار و کمتکرار را جداگانه بررسی کنید. دادهها را با تراکنشهای دستهای وارد کنید و پس از بارگذاری، آمار جدول را بهروز کنید. هر دو نسخه کوئری را در شرایط مشابه اجرا کنید: همان داده، همان مقدار فیلتر، همان اتصال و همان تعداد تکرار. اجرای نخست ممکن است هزینه گرمشدن cache داشته باشد، بنابراین زمان چند اجرای بعدی و دامنه تغییرات را گزارش کنید.
اندازهگیری را به زمان SQL محدود نکنید. تعداد query کل درخواست، زمان تبدیل مدلها، اندازه پاسخ و p95 درخواست را هم ثبت کنید. اگر ایندکس، read را سریع ولی درج سفارش را کند کرد، این هزینه باید در تصمیم دیده شود. ایندکس مرکب معمولاً وقتی سودمند است که شرط برابری روی ستون ابتدایی و ترتیب بعدی با کوئری هماهنگ باشند؛ اما انتخاب نهایی به cardinality، توزیع داده و الگوی write وابسته است.
چه زمانی کدام اصلاح را انتخاب کنیم؟
| نشانه | بررسی اول | اقدام محتمل |
|---|---|---|
| تعداد query با تعداد سفارش رشد میکند | رابطههایی که داخل حلقه خوانده میشوند | eager loading محدود یا بازطراحی داده پاسخ |
| یک query ردیفهای زیادی را بررسی میکند | EXPLAIN و توزیع فیلترها | بازنویسی شرط یا ایندکس هماهنگ |
| صفحههای انتهایی کند میشوند | OFFSET و ترتیب نتایج | pagination مبتنی بر cursor در فهرست مناسب |
| SQL سریع است اما endpoint کند است | serialization، API بیرونی و view | کاهش payload یا انتقال کار مستقل به صف |
این جدول ترتیب تشخیص است، نسخه همگانی نیست. برای مثال cursor pagination وقتی مناسب است که پیمایش پیوسته میخواهید؛ اگر کاربر باید مستقیم به صفحه ۴۲ برود، باید درباره محدودیت آن تصمیم بگیرید. در هر دو روش ترتیب یکتا لازم است تا سفارشها تکرار یا جاافتاده نشوند.
چکلیست تحویل تغییر
در پایان، pull request باید query قدیمی و جدید، خروجی EXPLAIN با حذف داده حساس، تعداد query درخواست، روش تولید داده آزمایشی و شرایط اندازهگیری را داشته باشد. یک بازه پایش پس از انتشار تعیین کنید و به خطای دیتابیس، مصرف CPU و تأخیر p95 نگاه کنید. اگر نتیجه مطابق انتظار نبود، اول تفاوت داده یا plan اجرا را بررسی کنید؛ بازگشت به نسخه قبل باید امکانپذیر باشد.
اگر مشکل از طراحی کلی مسیر درخواست است، نمونه پروژه لپمال و صفحه خدمات کارایی نشان میدهند چه نوع تحلیل فنی و خروجی قابل تحویلی را میتوان انتظار داشت. برای مطالعه متمرکز درباره کش، مقاله Redis را بخوانید؛ کش را پس از مشخصشدن علت کندی وارد تصمیم کنید.
برای بررسی جزئیات ابزارها، راهنمای رسمی EXPLAIN در MySQL و مستندات روابط و eager loading در Laravel مرجع مناسبی هستند.
پرسشهای متداول
چرا پروژه Laravel بعد از افزایش داده کند میشود؟
اغلب queryهایی که روی داده کم بیضرر بودند، بدون index یا با بارگذاری رابطههای اضافی روی حجم واقعی پرهزینه میشوند.
آیا Redis همه مشکلات سرعت را حل میکند؟
نه. کش میتواند فشار را کم کند، اما query بد، payload بزرگ یا طراحی اشتباه را پنهان میکند و مسئله invalidation تازهای میسازد.