الأداء: قِس قبل أن تحسّن
درس 1 من 4
1الأداء: قِس قبل أن تحسّن
الصورة الكبيرةالمطورون يخمّنون مكان البطء ويخطئون غالبًا. القياس يدلك على السطر الحقيقي، وغالبًا هو استعلام يتكرر في حلقة أو جدول بلا فهرس، لا خوارزمية معقدة.
ماذا تشاهد في الرسم: دورة الأداء: كل تحسين يبدأ بقياس وينتهي بقياس يثبت أنه أفاد.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
لو تأخرت عن العمل كل يوم، لا تشتري سيارة أسرع قبل أن تعرف أين يضيع الوقت. ربما 30 دقيقة تبحث عن المفتاح. القياس يدلك على المفتاح، والسيارة الأسرع توفر دقيقتين فقط.
المتوسط يخفي الأسوأ: p95 = 95% من الطلبات أسرع من هذا الرقم، وp99 يكشف الذيل الذي يعيشه أكثر المستخدمين نشاطًا. أدوات: cProfile وpy-spy في بايثون، وأدوات المتصفح وclinic.js في Node. N+1: استعلام للقائمة ثم استعلام لكل عنصر، يُحل بـ JOIN أو select_related/prefetch_related أو IN (...). الفهارس: EXPLAIN ANALYZE يكشف Seq Scan على جدول كبير، والفهرس المركّب يتبع ترتيب أعمدة الشرط. في الكود: البحث في قائمة O(n) داخل حلقة يصبح O(n²)، وset يجعله O(1). وقانون Amdahl: تحسين جزء يستهلك 5% من الوقت لا يوفر أكثر من 5%.
قبل أن تبدأ
🗄️SQL
JOIN والفهارس الأساسية.
🧮التعقيد الزمني
تعرف O(n) وO(n²).
الفكرة الجوهرية
1المئينات لا المتوسط
عشرة طلبات، تسعة سريعة وواحد بطيء جدًا. المتوسط يبدو مقبولًا، لكن p95 يكشف أن بعض المستخدمين ينتظرون ثانيتين:
import statistics
latencies_ms = [80, 85, 90, 95, 100, 110, 120, 130, 140, 2100]
def percentile(values, p):
ordered = sorted(values)
index = max(0, round(p / 100 * len(ordered)) - 1)
return ordered[index]
print("average:", statistics.mean(latencies_ms), "ms")
print("p50 :", percentile(latencies_ms, 50), "ms")
print("p95 :", percentile(latencies_ms, 95), "ms")2N+1: القاتل الصامت
نعدّ الاستعلامات بدل الزمن، لأن العدد هو المشكلة. صفحة فيها 50 طلبًا تعمل 51 استعلامًا، والحل استعلام واحد بـ JOIN:
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, total INTEGER);
""")
db.executemany("INSERT INTO customers VALUES (?, ?)", [(i, f"c{i}") for i in range(1, 11)])
db.executemany("INSERT INTO orders VALUES (?, ?, ?)", [(i, i % 10 + 1, i * 5) for i in range(1, 51)])
queries = 0
def run(sql, args=()):
global queries
queries += 1
return db.execute(sql, args).fetchall()
for order_id, customer_id, total in run("SELECT * FROM orders"):
run("SELECT name FROM customers WHERE id = ?", (customer_id,))
print("N+1 version :", queries, "queries")
queries = 0
rows = run("SELECT o.id, c.name, o.total FROM orders o JOIN customers c ON c.id = o.customer_id")
print("JOIN version:", queries, "query,", len(rows), "rows")3الفهرس وEXPLAIN
EXPLAIN يخبرك كيف ستنفذ القاعدة الاستعلام. قبل الفهرس تمسح الجدول كله، وبعده تقفز مباشرة:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, status TEXT)")
query = "SELECT id FROM orders WHERE customer_id = 7 AND status = 'new'"
def plan():
return db.execute("EXPLAIN QUERY PLAN " + query).fetchall()[0][-1]
print("before:", plan())
db.execute("CREATE INDEX idx_orders_customer_status ON orders (customer_id, status)")
print("after :", plan())EXPLAIN ANALYZE لترى الزمن الحقيقي، وابحث عن Seq Scan على جداول كبيرة.مثال محلول
المسألة: لوحة الإدارة تأخذ 6 ثوانٍ، والفريق يقترح إعادة كتابتها بـ Go. القياس: profiler يقول 95% من الوقت في القاعدة، و312 استعلامًا لكل تحميل. السبب: N+1 في قائمة الطلبات مع اسم العميل والفرع. الحل: JOIN واحد وفهرس على (status, created_at). النتيجة: 4 استعلامات و180ms، دون تغيير اللغة.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| اللغة الأسرع تحل البطء. | أغلب البطء في الشبكة والقاعدة، لا في سرعة اللغة. |
| فهرس على كل عمود أسرع. | كل فهرس يبطئ الكتابة ويأخذ مساحة. فهرس لما تبحث به فعلًا. |
اختبر فهمك
لماذا p95 أهم من المتوسط؟
طلب واحد بطيء جدًا يضيع في المتوسط.
صفحة بـ 100 عنصر تعمل 101 استعلام. ما المشكلة؟
استعلام للقائمة واستعلام لكل عنصر.
EXPLAIN يظهر Seq Scan على جدول بمليون صف. الحل الأول؟
الفهرس يحوّل المسح الكامل إلى قفزة مباشرة.
بطاقات مراجعة سريعة
تحقق من فهمك
ما أول ما تفعله مع شكوى بطء؟
تقيس: أي طلب، كم p95، وأين يُقضى الوقت.
كيف تجعل if x in items أسرع داخل حلقة؟
حوّل items إلى set مرة قبل الحلقة.
لماذا لا تظهر N+1 في التطوير؟
لأن البيانات قليلة، فـ 6 استعلامات لا تُلاحظ. عدّ الاستعلامات لا الزمن.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.