صفحه‌ی سفارش‌ها وقتی ده هزار ردیف داشت سریع بود. حالا دو میلیون ردیف دارد و هر بار باز کردنش چند ثانیه طول می‌کشد. کد عوض نشده، سرور هم همان است؛ تنها چیزی که عوض شده حجم داده است. در اغلب این موارد، مشکل نبود ایندکس مناسب است.

ایندکس را می‌شود با فهرست آخر کتاب مقایسه کرد. بدون آن، برای پیداکردن یک موضوع باید همه‌ی صفحه‌ها را ورق بزنید. با آن، مستقیم به صفحه‌ی درست می‌روید. این مقاله نشان می‌دهد چطور کوئری کند را پیدا کنید، با EXPLAIN بخوانید چه اتفاقی می‌افتد و ایندکس درست بسازید. اگر هنوز طراحی جدول‌هایتان را نهایی نکرده‌اید، طراحی جدول‌های دیتابیس را هم ببینید.

ابتدا کوئری کند را پیدا کنید#

حدس‌زدن کار درستی نیست. اول مطمئن شوید کدام کوئری‌ها کندند. MySQL قابلیت ثبت کوئری‌های کند دارد که می‌توان آن را روشن کرد (روی هاست اشتراکی ممکن است دسترسی نداشته باشید و باید از پشتیبانی بخواهید یا از ابزارهای کنترل‌پنل کمک بگیرید):

SQL
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

در وردپرس هم افزونه‌هایی هستند که کوئری‌های هر صفحه را نشان می‌دهند. به‌هرحال هدف این است که یک کوئری مشخص را جدا کنید و روی آن کار کنید.

خواندن EXPLAIN#

جدول orders را در نظر بگیرید:

SQL
CREATE TABLE orders (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL,
  status VARCHAR(20) NOT NULL,
  total DECIMAL(12,2) NOT NULL,
  created_at DATETIME NOT NULL
) ENGINE=InnoDB;

کوئری زیر آخرین سفارش‌های یک کاربر را می‌خواهد:

SQL
EXPLAIN SELECT id, total, created_at
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;

در خروجی این ستون‌ها را نگاه کنید:

ستونمعنینشانه‌ی بد
typeروش دسترسی به جدولALL یعنی خواندن کل جدول
keyایندکسی که واقعاً استفاده شدNULL یعنی هیچ ایندکسی
rowsتعداد ردیفی که MySQL تخمین می‌زند بخواندعدد خیلی بزرگ در برابر نتیجه‌ی کوچک
Extraتوضیح اضافیUsing filesort یعنی مرتب‌سازی جدا و پرهزینه

اگر type برابر ALL و key خالی باشد و Extra شامل Using filesort، همان چیزی را دیده‌اید که کوئری را کند کرده است. در نسخه‌های جدید MySQL می‌توانید EXPLAIN ANALYZE را هم امتحان کنید که زمان واقعی اجرا را نشان می‌دهد.

ساخت ایندکس#

برای این کوئری، یک ایندکس ترکیبی روی user_id و created_at مناسب است:

SQL
ALTER TABLE orders
  ADD INDEX idx_orders_user_created (user_id, created_at);

حالا دوباره EXPLAIN را اجرا کنید. باید key همین ایندکس باشد و rows کمتر شود. چون ایندکس هم فیلتر user_id و هم ترتیب created_at را پوشش می‌دهد، مرتب‌سازی جدا هم لازم نیست.

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

ترتیب ستون‌ها در ایندکس ترکیبی مهم است#

ایندکس (user_id, created_at) مثل دفترچه‌ی تلفنی است که اول بر اساس نام خانوادگی و بعد بر اساس نام مرتب شده. شما می‌توانید با نام خانوادگی جست‌وجو کنید، یا با هر دو؛ ولی فقط با نام کوچک نه. قاعده‌ی عملی:

  1. ستون‌هایی که با = مقایسه می‌شوند را اول بگذارید.
  2. بعد ستونی که برای بازه (>، <، BETWEEN) یا ترتیب (ORDER BY) استفاده می‌شود.
  3. ایندکس (a, b) به‌جای ایندکس جدای (a) هم کار می‌کند، پس ساخت هر دو معمولاً لازم نیست.

چیزهایی که ایندکس را بی‌اثر می‌کنند#

حتی با ایندکس درست، بعضی شکل‌های نوشتن کوئری آن را بی‌فایده می‌کنند:

SQL
-- بد: تابع روی ستون، ایندکس created_at استفاده نمی‌شود
SELECT id FROM orders WHERE DATE(created_at) = '2026-10-01';

-- خوب: بازه روی خود ستون
SELECT id FROM orders
WHERE created_at >= '2026-10-01' AND created_at < '2026-10-02';

-- بد: LIKE با علامت درصد در ابتدا
SELECT id FROM users WHERE email LIKE '%@example.com';

-- بهتر: LIKE که از ابتدای مقدار شروع می‌شود
SELECT id FROM users WHERE email LIKE 'ali%';

مشکل دیگر مقایسه‌ی نوع‌های ناهماهنگ است؛ مثلاً ستون رشته‌ای را با عدد مقایسه کنید. MySQL ممکن است مجبور شود مقدار هر ردیف را تبدیل کند و ایندکس بی‌اثر شود. برای جست‌وجوی متن فارسی هم به جستجوی فارسی در MySQL نگاه کنید، چون مشکل‌های آن از جنس دیگری است.

ایندکس پوششی#

اگر همه‌ی ستون‌هایی که کوئری می‌خواهد داخل خود ایندکس باشند، MySQL اصلاً سراغ جدول اصلی نمی‌رود. به این حالت ایندکس پوششی می‌گویند و در EXPLAIN با عبارت Using index در ستون Extra دیده می‌شود. برای کوئری بالا می‌توانید total را هم به ایندکس اضافه کنید:

SQL
ALTER TABLE orders
  ADD INDEX idx_orders_user_created_total (user_id, created_at, total);

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

هزینه‌ی ایندکس#

ایندکس مجانی نیست. هر INSERT، UPDATE و DELETE باید ایندکس‌ها را هم به‌روز کند و هر ایندکس فضای دیسک می‌خواهد. پس:

  • ایندکس‌های تکراری و استفاده‌نشده را پیدا و حذف کنید.
  • ستون‌هایی با تنوع بسیار کم (مثل جنسیت یا وضعیت دودویی) به‌تنهایی ایندکس خوبی نیستند.
  • بعد از هر ایندکس تازه با EXPLAIN مطمئن شوید واقعاً استفاده می‌شود.

برای حذف ایندکسی که فایده ندارد:

SQL
ALTER TABLE orders DROP INDEX idx_orders_user_created;

خلاصه#

  • اول کوئری کند را شناسایی کنید و بعد ایندکس بسازید؛ حدس‌زدن کافی نیست.
  • EXPLAIN را بخوانید: type=ALL، key=NULL و Using filesort نشانه‌های مشکل‌اند.
  • ایندکس ترکیبی را با ستون‌های مقایسه‌ی برابر شروع و با ستون بازه یا ترتیب تمام کنید.
  • تابع روی ستون و LIKE '%...' ایندکس را بی‌اثر می‌کنند.
  • هر ایندکس هزینه‌ی نوشتن و فضا دارد؛ فقط برای کوئری‌های واقعی بسازید.
  • قبل از تغییر روی جدول بزرگ، پشتیبان بگیرید.