قواعد البيانات والتخزين

إلغاء التسوية ونمذجة البيانات للتوسع

18 دقيقة الدرس 7 من 10

إلغاء التسوية ونمذجة البيانات للتوسع

في مقررات تصميم قواعد البيانات تتعلم التسوية (Normalization): التخلص من الازدواجية، وتطبيق التكامل المرجعي، والإبقاء على كل حقيقة في مكان واحد بالضبط. التسوية صحيحة — لتطبيق صغير بحركة مرور معتدلة. لكن حين يخدم نظامك عشرات الملايين من المستخدمين ويجب أن يُجيب على استعلامات معقدة في أقل من عشرة ميلي ثانية، تصبح التسوية الصارمة عائقًا. إلغاء التسوية (Denormalization) هو عكس هذه العملية بشكل متعمد ومدروس: تُكرر البيانات، وتُحسب النتائج مسبقًا، أو تُعيد هيكلة الجداول لخدمة أنماط وصول بعينها بأسرع ما يمكن. والكلمة المفتاحية هي متعمد — إلغاء التسوية بشكل أعمى يُفضي إلى كوابيس من البيانات غير المتسقة. أما المنهجي منه، فهو ما يُفرّق بين التطبيقات الهواة وأنظمة الإنتاج على نطاق واسع.

لماذا تنهار التسوية عند التوسع؟

تأمل خلاصة أخبار (feed) على منصة تواصل اجتماعي. قد يبدو المخطط الكامل التسوية هكذا: users، posts، follow_edges، likes، comments. لعرض خلاصة أخبار مستخدم يتابع 500 شخص، تحتاج إلى استعلام يقوم بما يلي:

  1. ضم follow_edges لاسترداد معرّفات الـ500 مستخدم المتابَعين.
  2. الاستعلام عن posts للمنشورات الحديثة من هؤلاء الـ500.
  3. ضم likes وcomments للحصول على الأعداد.
  4. ضم users مرة أخرى للحصول على أسماء العرض وروابط الصور.

على عقدة واحدة، هذا يعمل بسهولة مع 1,000 مستخدم. مع 100 مليون مستخدم وكل مستخدم يُحمّل خلاصته كل بضع ثوانٍ، يصبح الاستعلام المليء بعمليات الضم بطيئًا باستحالة، ولا يمكن تجزئة قاعدة البيانات (sharding) لأن الضم يمتد عبر مستخدمين على كل الأجزاء المحتملة. يحتاج النظام إلى نموذج بيانات مختلف جوهريًا.

القاعدة الذهبية للتصميم الموجّه للتوسع: صمّم بياناتك حول أنماط الوصول، لا حول الكيانات نفسها. في الحجم الصغير، طبّق التسوية للصحة. مع التوسع، ألغِ التسوية لأداء القراءة — لكن فقط في المسارات الحارة التي تحتاجه فعلًا.

تقنيات إلغاء التسوية الأساسية

1. تكرار الأعمدة لتجنب عمليات الضم

بدلاً من ضم جدول users في كل مرة تسترد فيها منشورًا، خزّن author_name وauthor_avatar_url مباشرةً في جدول posts. حين يُغيّر المستخدم اسمه، تُحدّث المكانين — جدول users وكل صف منشور كتبه. هذا مقبول حين تكون عمليات الكتابة نادرة (الأسماء نادرًا ما تتغير) وعمليات القراءة متكررة جدًا (كل تحميل للخلاصة).

تستخدم تويتر هذا النمط بالضبط: صف التغريدة يحتوي على اسم المؤلف وعدد متابعيه وقت الكتابة. تحديث 500 مليون صف تغريدة حين يُغيّر مستخدم اسمه بالطبع لا يتم بشكل متزامن — إما أن تُقبل على أنها اتساق لاحق، أو تُعالَج بواسطة وظيفة خلفية.

2. تخزين الأعداد المُجمَّعة / المُشتقة

تشغيل SELECT COUNT(*) FROM likes WHERE post_id = ? عند كل تحميل صفحة مُسرف حين يمتلك منشور 2 مليون إعجاب. بدلاً من ذلك، احتفظ بعمود like_count مباشرةً في صف posts. زِده بشكل ذري (UPDATE posts SET like_count = like_count + 1 WHERE id = ?) عند كل إعجاب. تصبح القراءة جلب عمود واحد؛ تتقبّل تكلفة كتابة إضافية صغيرة مقابل توفير هائل في القراءة.

3. جداول الخلاصة / صندوق الوارد المُجهّزة مسبقًا

بدلاً من حساب خلاصة المستخدم وقت القراءة، اكتبها وقت الكتابة. حين ينشر المستخدم A شيئًا، يقوم عامل خلفي بتوزيع المنشور إلى جدول feeds لكل متابع. خلاصة كل مستخدم قائمة معرّفات منشورات مُرتبة مسبقًا. قراءة الخلاصة الآن مسح نطاق بسيط على جدول واحد مُفهرس — لا ضم، لا تجميع. هذا يُسمى نمط التوزيع عند الكتابة (fan-out-on-write). استخدمت إنستغرام وتويتر المبكرة (للمستخدمين الذين لديهم أقل من مليون متابع تقريبًا) هذا النموذج.

4. تضمين المستندات (NoSQL)

في قواعد البيانات الوثائقية كـ MongoDB، ما يعادل إلغاء التسوية هو التضمين (embedding): بدلاً من الإشارة إلى كيان مرتبط بمعرّفه، تُضمن بياناته مباشرةً داخل المستند الأصل. قد يُضمّن مستند طلب تجارة إلكترونية اللقطة الكاملة للمنتج (الاسم، السعر، SKU، رابط الصورة) وقت الطلب، لأنك لا تريد أبدًا أن يتغير الطلب التاريخي إذا تغيّر السعر الحالي للمنتج. هذا صحيح دلاليًا — الطلب يمثّل ما اشتُري في تلك اللحظة — ويجعل قراءة الطلب جلب مستند واحد.

منهجية نمط الوصول

العملية الصحيحة لنمذجة بيانات الحجم الكبير هي:

  1. أحصِ أهم 5-10 استعلامات قراءة حسب التكرار ومتطلب زمن الاستجابة. هذه الاستعلامات ستكون على المسار الحرج لكل تحميل صفحة.
  2. أحصِ عمليات الكتابة وتكرارها.
  3. صمّم مخططًا يُجيب على كل قراءة في بحث واحد مُفهرس (لا ضمّ متعدد الجداول على المسارات الحارة). اقبل أن الكتابات ستبذل جهدًا أكبر.
  4. احتفظ بمصدر حقيقة مُسوّى للبيانات التي تحتاج إلى اتساق، واشتق طرق العرض المُقروءة غير المُسوّاة منه بشكل غير متزامن.
Normalized vs denormalized feed read path Normalized — Read at Query Time App: GET /feed follow_edges table posts table users table likes table Merge 4+ table results High latency — many round trips Cannot shard across all tables Denormalized — Pre-materialized Feed App: GET /feed feeds table (user_id index) Row: post_id, author_name, author_avatar, like_count, ts (all data pre-joined at write) Return rows directly Low latency — single range scan Easily sharded by user_id
المخطط المُسوّى يتطلب ضم 4+ جداول لكل تحميل خلاصة؛ جدول الخلاصة المُجهَّز مسبقًا يُجيب على نفس الطلب بمسح نطاق واحد مُفهرس.

تضخيم الكتابة: تكلفة إلغاء التسوية

تجهيز البيانات مسبقًا ليس مجانيًا. كل كتابة الآن تُفجّر كتابات متعددة:

  • منشور جديد من مستخدم لديه مليون متابع يجب كتابته في مليون صف في جدول الخلاصة. إذا كان المنشور 200 بايت والمستخدم لديه مليون متابع، فإن حدث كتابة واحد يُنتج ~200 ميغابايت من البيانات المشتقة.
  • تغيير المستخدم لاسمه يتطلب تحديث النسخة المكررة في كل جدول يخزنها.

تتعامل الصناعة مع هذا عبر استراتيجيات عدة:

  • التوزيع الهجين (Hybrid fan-out): جهّز الخلاصات مسبقًا فقط للمستخدمين الذين لديهم أقل من N متابع (مثلاً 10,000). للمشاهير الذين لديهم ملايين المتابعين، اجلب منشوراتهم وقت القراءة وادمجها. هذا بالضبط ما تسميه تويتر (X الآن) "وضع المشاهير (celebrity mode)".
  • عمال الخلفية غير المتزامنين: اكتب الصف الأصلي بشكل متزامن، ثم أرسل رسالة إلى طابور المهام للتوزيع على طرق العرض المقروءة. المستخدم يرى خلاصة بالتسق اللاحق (eventually consistent)، وهو مقبول لمعظم تطبيقات التواصل الاجتماعي.
  • إلغاء التسوية المحدود: فقط ألغِ تسوية الأعمدة التي نادرًا ما تتغير. اسم المؤلف والصورة ومحتوى المنشور نادرًا ما تتغير؛ أعداد الإعجاب تتغير باستمرار لكن يمكن تحديثها بزيادة ذرية واحدة.
قِس قبل أن تُلغي التسوية. استخدم مخطط الاستعلام وسجل الاستعلامات البطيئة وتتبعات APM للعثور على الاستعلامات الفعلية في المسار الحار. ألغِ التسوية فقط لأنماط الوصول التي تظهر في القياسات على أنها عنق الزجاجة. إلغاء التسوية المبكر يُضيف تعقيد الكتابة دون فائدة ملموسة.

جداول الأعمدة العريضة: النمذجة للسلاسل الزمنية على نطاق واسع

تُطبّق Cassandra وما شابهها من قواعد الأعمدة العريضة مبدأ نمط الوصول بشكل متطرف. في Cassandra، تُصمّم جدولاً لكل استعلام. إذا احتجت "آخر 100 رسالة في محادثة"، تُنشئ جدولاً بمفتاح أساسي مركّب من (conversation_id, message_timestamp)، مُجزّأ حسب conversation_id ومُجمَّع (مُرتَّب) حسب message_timestamp DESC. الاستعلام دائمًا بحث بمفتاح الجزء ثم مسح نطاق على عمود التجميع — عمليتان قابلتان للتنبؤ وسريعتان بصرف النظر عن حجم مجموعة البيانات. قد تُخزَّن نفس البيانات في ثلاثة أو أربعة جداول Cassandra منفصلة للإجابة على ثلاثة أو أربعة أشكال استعلام مختلفة.

Wide-column table design for messaging Query: last 100 msgs in conv #42 Partition Key Clustering Key Columns conv_id = 42 ts = 2024-06-09 14:02:11 sender=Alice, text="Hey!" conv_id = 42 ts = 2024-06-09 14:02:35 sender=Bob, text="Hi there" conv_id = 42 ts = 2024-06-09 14:03:10 sender=Alice, text="Busy?" … (all conv_id=42 rows sorted by ts DESC, co-located on same partition) … All on ONE partition node Sorted by timestamp — range scan = sequential I/O
في قاعدة الأعمدة العريضة، جميع رسائل المحادثة رقم 42 موجودة في جزء واحد، مُرتبة مسبقًا حسب الوقت — مسح النطاق يقرأ 100 صف بعمليات I/O متسلسلة بدون أي ضم.

إرشادات عملية

  • سوِّ أولاً، ثم ألغِ التسوية لاحقًا. ابدأ بمخطط مُسوّى ونظيف. قِس. ألغِ التسوية فقط في المسارات التي تُثبتها القياسات على أنها عنق الزجاجة.
  • احتفظ بمصدر الحقيقة. احتفظ دائمًا بسجل موثوق وحيد لكل كيان. النسخ غير المُسوّاة مُشتقة منه، وليست مرجعية.
  • اختر الاتساق اللاحق بوعي. حين توزّع الكتابات بشكل غير متزامن، ستكون طرق العرض المقروءة مؤقتًا قديمة. وثّق التأخير المقبول (مثلاً: SLA انتعاش الخلاصة بـ2 ثانية) وأبلغ به المنتج والمستخدمين.
  • تجنب إلغاء تسوية البيانات المتقلبة. إذا كانت قيمة تتغير عشرات المرات في الثانية (سعر مزاد مباشر، أعداد المشاهدات الآنية)، لا تُضمّنها في كل سجل مرتبط. اجلبها بشكل منفصل من خدمة عدّادات مخصصة أو ذاكرة مؤقتة.
  • استخدم طرق العرض المادية حيثما دعمتها قاعدة البيانات. تدعم PostgreSQL وبعض قواعد البيانات التحليلية طرق عرض مادية تُحدّثها قاعدة البيانات بنفسها، مما يتيح لك الاستعلام عن بيانات غير مُسوّاة دون إدارة منطق التوزيع في كود التطبيق.
إلغاء التسوية بدون انضباط يُنتج أخطاء بيانات قديمة يصعب جدًا تصحيحها. نمط الإخفاق الشائع: تُخزّن اسم المؤلف في 15 جدولاً. يُغيّر المستخدم اسمه. منطق التحديث يفوته جدولان بسبب خطأ في الكود. الآن 15% من منشوراته تُظهر الاسم الخاطئ. بعد أشهر يُقدّم عميل شكوى. الإصلاح يتطلب ترحيلاً خلفيًا عبر ملايين الصفوف. دائمًا تعامل مع كل عمود مُكرَّر على أنه التزام يتطلب مسارًا واضحًا وخاضعًا للاختبار للتحديث.

ملخص

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