كنا نسأل قاعدة البيانات عن كل شاردة وواردة: كيف أنقذتنا ‘مرشحات بلوم’ (Bloom Filters) من جحيم استعلامات التحقق؟

يا جماعة الخير، السلام عليكم ورحمة الله.

اسمحوا لي اليوم أن أرجع بالذاكرة لسنوات قليلة مضت، لأحد المشاريع التي كنت أعمل عليها مع فريق من المبرمجين النشامى. كنا نبني منصة اجتماعية جديدة، وكان الحماس “ولعان” بيننا. أحد أهم متطلبات النظام، كما هو الحال في أي منصة، هو التحقق من أن اسم المستخدم الجديد لم يُستخدم من قبل عند تسجيل أي عضو جديد.

في البداية، كانت الأمور بسيطة. مع كل طلب تسجيل، كنا نرسل استعلاماً بسيطاً لقاعدة البيانات، شيء من هذا القبيل:

SELECT EXISTS(SELECT 1 FROM users WHERE username = 'new_username_here');

كانت هذه الطريقة تعمل كالسحر في المراحل الأولى. ولكن مع نمو قاعدة المستخدمين، بدأت الكارثة تتكشف. تخيلوا معنا آلاف المستخدمين يحاولون التسجيل في نفس الوقت، وكل محاولة فاشلة لاختيار اسم مستخدم (لأنه مستخدم بالفعل) تعني استعلاماً يضرب قاعدة البيانات. أصبحت لوحة مراقبة أداء الخوادم حمراء بالكامل، وقاعدة البيانات تئن تحت وطأة هذه الاستعلامات الصغيرة والمكلفة التي لا تنتهي. كنا حرفياً نسألها عن كل شاردة وواردة، وكأنها موظف أرشيف منهك نرسله ليفتش في آلاف الملفات فقط ليقول لنا “موجود” أو “غير موجود”.

في أحد اجتماعات الفريق الطارئة، والتوتر سيد الموقف، تذكرت محاضرة حضرتها في الجامعة عن هياكل البيانات “الاحتمالية” (Probabilistic Data Structures). لمعت في ذهني فكرة، وقلت لهم: “يا شباب، شو القصة؟ إحنا بنحرق موارد السيرفر على سؤال بسيط. فيه حل أذكى من هيك، اسمه مرشح بلوم!”.

ما هو الجحيم الذي كنا نعيش فيه؟ (مشكلة استعلامات التحقق)

قبل أن نغوص في الحل، دعونا نفهم عمق المشكلة. المشكلة لم تكن في الاستعلام بحد ذاته، بل في “تكراره” و”تكلفته” التراكمية. كل استعلام، مهما كان بسيطاً، يستهلك موارد:

  • اتصال بالشبكة (Network Latency): يستغرق وقتًا لكي ينتقل الطلب من خادم التطبيق إلى خادم قاعدة البيانات ويعود بالرد.
  • استهلاك موارد قاعدة البيانات (DB Overhead): حتى أبسط استعلام يحتاج إلى تحليل، وتخطيط للتنفيذ، وربما قراءة من القرص الصلب (Disk I/O)، وهي أبطأ عملية في عالم الحوسبة.
  • استنزاف مجمع الاتصالات (Connection Pooling): كل استعلام يحجز اتصالاً من مجمع الاتصالات المحدود، مما يمنع استعلامات أخرى أكثر أهمية من التنفيذ بسرعة.

كانت مشكلتنا أن 99% من هذه الاستعلامات تعود بنتيجة “نعم، الاسم موجود”، مما يعني أن المستخدم سيجرب اسماً آخر، وهذا يعني… استعلاماً جديداً! كنا في حلقة مفرغة من إهدار الموارد على معلومة يمكن الحصول عليها بطريقة أرخص بكثير.

المنقذ: مرشحات بلوم (Bloom Filters)

مرشح بلوم ليس خوارزمية بالمعنى التقليدي، بل هو “هيكل بيانات احتمالي” موفر جداً للمساحة، ومصمم ليجيب عن سؤال واحد فقط: “هل هذا العنصر عضو في هذه المجموعة؟”.

لكن هنا تكمن الخدعة الذكية: إجابته ليست دائماً “نعم” أو “لا” بشكل قاطع. بل هي واحدة من اثنتين:

  1. “هذا العنصر بالتأكيد ليس في المجموعة.” (No, definitely not)
  2. “هذا العنصر ربما يكون في المجموعة.” (Yes, possibly)

لاحظوا الفرق الدقيق. مرشح بلوم لا يعطي أبداً نتيجة “سلبية كاذبة” (False Negative). أي أنه إذا قال لك إن العنصر غير موجود، فهو 100% غير موجود. لكنه قد يعطي نتيجة “إيجابية كاذبة” (False Positive)، أي أنه قد يقول لك إن العنصر “ربما موجود” بينما هو في الحقيقة غير موجود. وسنشرح السبب لاحقًا.

كيف تعمل هذه “الآلية السحرية”؟

تخيل مرشح بلوم على أنه صف طويل جداً من مفاتيح الإضاءة الصغيرة (bits)، وكلها في البداية في وضع “إطفاء” (قيمتها 0).

مرحلة الإضافة (إضافة عنصر جديد)

عندما نريد إضافة عنصر (مثل اسم مستخدم “abu_omar”) إلى المرشح، نقوم بالخطوات التالية:

  1. نمرر العنصر “abu_omar” على عدد معين من دوال التجزئة (Hash Functions) المختلفة والمستقلة، ولنقل 3 دوال.
  2. كل دالة تجزئة ستعطينا رقماً مختلفاً، وهذا الرقم يمثل “موقع” مفتاح إضاءة معين في الصف الطويل.
  3. نذهب إلى هذه المواقع الثلاثة ونقوم بتشغيل مفاتيح الإضاءة فيها (نغير قيمتها من 0 إلى 1).

نكرر هذه العملية لكل أسماء المستخدمين الموجودة لدينا في قاعدة البيانات. بالتدريج، سيمتلئ صف مفاتيح الإضاءة هذا بالكثير من المفاتيح المضاءة.

مرحلة التحقق (التحقق من وجود عنصر)

الآن، عندما يأتي مستخدم جديد ويحاول التسجيل باسم “user123″، نقوم بالآتي قبل أن نسأل قاعدة البيانات:

  1. نمرر الاسم “user123” على نفس دوال التجزئة الثلاث.
  2. نحصل على ثلاثة مواقع في صف مفاتيح الإضاءة.
  3. نذهب ونتفحص هذه المواقع الثلاثة:
    • إذا كان واحد فقط (أو أكثر) من هذه المفاتيح في وضع “إطفاء” (قيمته 0)، فهذا يعني أن هذا العنصر “مستحيل” أن يكون قد أُضيف من قبل. لأننا لو أضفناه، لكانت كل مفاتيحه مضاءة. هنا، يمكننا أن نقول للمستخدم فوراً “الاسم متاح” دون لمس قاعدة البيانات. وهذا هو الانتصار الكبير!
    • إذا كانت كل المفاتيح الثلاثة في وضع “إضاءة” (قيمتها 1)، فهذا يعني أن العنصر “ربما” يكون موجوداً. لماذا “ربما”؟ لأنه من المحتمل أن تكون هذه المفاتيح الثلاثة قد أضيئت بسبب عناصر أخرى مختلفة تماماً تصادف أن دوال التجزئة الخاصة بها أشارت إلى نفس هذه المفاتيح. هذا ما نسميه “الإيجابية الكاذبة”.

نصيحة من أبو عمر: جمال مرشح بلوم يكمن في قدرته على تصفية الغالبية العظمى من الطلبات السلبية (“هذا العنصر غير موجود”) بتكلفة تقترب من الصفر (مجرد عملية في الذاكرة)، مما يحرر قاعدة بياناتك للتعامل مع الحالات التي تحتاج إلى تحقق فعلي.

المعضلة الأزلية: الإيجابيات الكاذبة (False Positives)

كما ذكرنا، مرشح بلوم ليس مثالياً. احتمالية حدوث “إيجابية كاذبة” تعتمد على عاملين رئيسيين يمكنك التحكم بهما:

  • حجم المرشح (m): حجم مصفوفة البتات (عدد مفاتيح الإضاءة). كلما كان أكبر، قلّت فرصة تصادم التجزئات.
  • عدد دوال التجزئة (k): عدد الدوال التي تستخدمها. عدد أكبر يوزع “البصمة” بشكل أفضل، ولكن إلى حد معين، فإذا زاد عن حده سيملأ المرشح بسرعة.

لحسن الحظ، هناك معادلات رياضية تسمح لك بحساب الحجم الأمثل للمرشح وعدد دوال التجزئة بناءً على عدد العناصر المتوقع تخزينها ونسبة الإيجابيات الكاذبة التي يمكنك تحملها (مثلاً 1%).

في حالتنا، نسبة 1% كانت مقبولة تماماً. هذا يعني أن من بين كل 100 اسم مستخدم جديد ومتاح، قد يضطر النظام للذهاب إلى قاعدة البيانات في حالة واحدة فقط للتأكد، بينما سيوفر 99 استعلاماً! هذا تحسن هائل.

نرجع لقصتنا: كيف طبقنا الحل عملياً؟

كانت الخطة كالتالي:

  1. عند بدء تشغيل خادم التطبيق، نقوم بإنشاء مرشح بلوم في الذاكرة.
  2. نقوم بتنفيذ استعلام واحد ضخم لقاعدة البيانات لجلب كل أسماء المستخدمين الموجودة.
  3. نقوم بإضافة كل هذه الأسماء إلى مرشح بلوم في الذاكرة.
  4. الآن، مع كل طلب تسجيل جديد، نطبق المنطق الذي شرحناه.

هذا مثال بسيط جداً بلغة بايثون يوضح الفكرة باستخدام مكتبة pybloom_live:


from pybloom_live import BloomFilter
import time

# الخطوة 1 و 2: لنفترض أننا جلبنا هذه الأسماء من قاعدة البيانات عند بدء التشغيل
existing_usernames = [f'user{i}' for i in range(100000)] # 100 ألف مستخدم حالي
print(f"لدينا {len(existing_usernames)} مستخدم في قاعدة البيانات.")

# الخطوة 3: إنشاء وتعبئة مرشح بلوم
# ننشئه بسعة 200 ألف (لإعطاء مساحة للنمو) وبنسبة خطأ 0.1%
# The library will calculate the optimal number of hashes for us.
bloom = BloomFilter(capacity=200000, error_rate=0.001)

print("بدء تعبئة مرشح بلوم...")
start_time = time.time()
for username in existing_usernames:
    bloom.add(username)
end_time = time.time()
print(f"انتهت التعبئة في {end_time - start_time:.2f} ثانية.")

# --- الآن، الخادم جاهز لاستقبال الطلبات ---

def check_username_availability(username):
    # الخطوة 4: التحقق من خلال مرشح بلوم أولاً
    if username in bloom:
        # إيجابية محتملة (قد تكون كاذبة)
        print(f"المرشح يقول: الاسم '{username}' قد يكون مستخدماً. سنتحقق من قاعدة البيانات...")
        # هنا فقط نذهب إلى قاعدة البيانات
        # in_db = db.execute("SELECT EXISTS(...)")
        # في مثالنا، سنتحقق من القائمة الأصلية
        if username in existing_usernames:
            print(f"قاعدة البيانات تؤكد: الاسم '{username}' مستخدم بالفعل.")
            return False
        else:
            # هذه هي حالة الإيجابية الكاذبة!
            print(f"قاعدة البيانات توضح: الاسم '{username}' متاح! (كانت إيجابية كاذبة).")
            return True
    else:
        # سلبية مؤكدة
        print(f"المرشح يؤكد: الاسم '{username}' متاح بالتأكيد. لا داعي للذهاب لقاعدة البيانات.")
        return True

# --- اختبارات ---
print("n--- اختبار اسم مستخدم موجود بالفعل ---")
check_username_availability('user5000')

print("n--- اختبار اسم مستخدم جديد تماماً ---")
check_username_availability('new_awesome_user_123')

print("n--- اختبار حالة إيجابية كاذبة (قد تحدث أو لا) ---")
# سنجرب اسماً عشوائياً حتى نجد حالة إيجابية كاذبة
import random
import string
while True:
    random_user = ''.join(random.choice(string.ascii_lowercase) for i in range(10))
    if random_user not in existing_usernames and random_user in bloom:
        check_username_availability(random_user)
        break

النتيجة كانت مذهلة. انخفض الحمل على قاعدة البيانات بنسبة تتجاوز 90%. أصبح النظام أسرع وأكثر استجابة، وشعرنا كفريق أننا حققنا “ضربة معلم” حقيقية.

نصيحة من أبو عمر: متى تستخدم مرشحات بلوم (ومتى تهرب منها)؟

هذه الأداة قوية، لكنها ليست حلاً لكل المشاكل. إليك خلاصة خبرتي:

حالات استخدام مثالية 👍

  • التحقق من تفرد البيانات: أسماء المستخدمين، البريد الإلكتروني، عناوين URL المختصرة.
  • تجنب تكرار العمل: في أنظمة التوصية، لتصفية المقالات أو المنتجات التي شاهدها المستخدم بالفعل.
  • أنظمة التخزين المؤقت (Caching): تستخدمها شبكات توصيل المحتوى (CDNs) لتحديد ما إذا كان الملف “غير موجود” في خادم التخزين المؤقت القريب، لتجنب البحث البطيء في القرص الصلب. جوجل كروم يستخدمها للتحقق من المواقع الخبيثة.
  • قواعد البيانات الموزعة: تستخدمها أنظمة مثل Cassandra و Bigtable لتقليل عمليات البحث عبر الشبكة. قبل البحث عن مفتاح في عشرات الخوادم، يتم التحقق من مرشحات بلوم الخاصة بها أولاً.

حالات يجب تجنبها 👎

  • عندما تكون الإيجابيات الكاذبة غير مقبولة إطلاقاً: إذا كان نظامك لا يستطيع تحمل أي نسبة خطأ، فهذه الأداة ليست لك.
  • عندما تحتاج إلى حذف العناصر: مرشح بلوم القياسي لا يدعم الحذف. لا يمكنك ببساطة تغيير البت من 1 إلى 0، لأن هذا البت قد يكون جزءاً من “بصمة” عنصر آخر. (توجد بدائل أكثر تعقيداً مثل Counting Bloom Filters تسمح بالحذف ولكنها تستهلك ذاكرة أكبر).

الخلاصة: ضربة معلم بدل ألف استعلام! 🎯

في نهاية المطاف، قصة نجاحنا مع مرشحات بلوم تعلمنا درساً قيّماً: في هندسة البرمجيات، لا يتعلق الأمر دائماً بزيادة قوة الخوادم أو شراء أجهزة أغلى ثمناً. في كثير من الأحيان، الحل يكمن في اختيار هيكل البيانات الصحيح للمشكلة الصحيحة.

مرشح بلوم هو مثال رائع على كيف يمكن لفكرة “احتمالية” بسيطة أن توفر كمية هائلة من الموارد الحاسوبية، وتحسن الأداء بشكل جذري، وتحول نظاماً على حافة الانهيار إلى نظام سريع وفعال. إنه بمثابة حارس بوابة ذكي ورخيص، يمنع 99% من الزوار غير المرغوب فيهم من إزعاج الملك (قاعدة البيانات).

نصيحتي الأخيرة لكم: لا تخافوا من هياكل البيانات الخوارزميات الاحتمالية. أحياناً، إجابة “ربما” هي كل ما تحتاجه لبناء أنظمة أسرع وأكثر قابلية للتوسع. فكروا خارج الصندوق، فغالباً ما تكون الحلول الأكثر أناقة مخبأة في بساطتها.

ودمتم سالمين يا نشامى.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

​معمارية البرمجيات

كانت خدماتنا ككرة صوف: كيف حررتنا ‘المعمارية الموجهة بالأحداث’ (EDA) من جحيم التبعيات؟

أتذكر ليلة إطلاق تلك الميزة الجديدة وكأنها البارحة، كيف تحولت فرحة الفريق إلى كابوس بسبب خدمة واحدة أوقفت النظام بأكمله. في هذه المقالة، أشارككم قصة...

26 أبريل، 2026 قراءة المزيد
ذكاء اصطناعي

كان أداء نماذجنا يتدهور بصمت: كيف أنقذنا رصد انحراف البيانات (Data Drift) من جحيم التنبؤات الفاسدة؟

في عالم الذكاء الاصطناعي، أذكى النماذج يمكن أن تصبح غبية مع مرور الوقت. أشارككم قصة حقيقية عن كيفية تدهور أداء نماذجنا بصمت بسبب "انحراف البيانات"...

26 أبريل، 2026 قراءة المزيد
تسويق رقمي

ميزانيتنا التسويقية كانت ثقباً أسود: كيف أنقذنا ‘نموذج الإحالة المبني على البيانات’ من جحيم تخمين العائد على الاستثمار؟

أشارككم قصة حقيقية من قلب معارك الميزانيات والتسويق الرقمي. كنا نغرق في بحر من التخمينات حول عائد استثمارنا، حتى ظهر "نموذج الإحالة المبني على البيانات"...

26 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

واجهاتنا كانت فوضى بصرية: كيف أنقذنا ‘نظام التصميم’ (Design System) من جحيم عدم الاتساق؟

أشارككم قصتي كـ"أبو عمر" مع الفوضى التي عمت واجهاتنا الرقمية، وكيف كان "نظام التصميم" هو طوق النجاة. هذه ليست مجرد مقالة تقنية، بل هي حكاية...

26 أبريل، 2026 قراءة المزيد
برمجة وقواعد بيانات

كان بحثنا بطيئاً وغير دقيق: كيف أنقذنا البحث كامل النص (Full-Text Search) من جحيم استعلامات LIKE؟

أتذكر جيداً صرخات المستخدمين من بطء البحث وعدم دقته في أحد مشاريعنا. في هذه المقالة، أشارككم قصتي مع التخلي عن استعلامات LIKE الكارثية والانتقال إلى...

26 أبريل، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

مقابلات تصميم النظم كانت كابوسي: كيف أنقذني إطار عمل منهجي من جحيم ‘سنتواصل معك لاحقاً’؟

هل تجمدت يوماً أمام السبورة البيضاء في مقابلة تصميم النظم؟ أنا أبو عمر، وهذا ما حدث معي. في هذه المقالة، أشارككم قصتي وإطار العمل المنهجي...

26 أبريل، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

انهيار النظام بسبب خدمة واحدة؟ كيف أنقذنا نمط قاطع الدائرة (Circuit Breaker) من الفشل المتتالي

أشارككم قصة حقيقية من قلب المعركة التقنية، حين كان خطأ في خدمة صغيرة يكاد أن يدمر نظامنا بأكمله. سنغوص في تفاصيل نمط "قاطع الدائرة" (Circuit...

26 أبريل، 2026 قراءة المزيد
البودكاست