DanLevy.net

المفاتيح الخارجية: توقف عن السؤال عن سرعتها

اسأل ما الذي تحسّنه فعليًا.

أغلى عملية تحسين لقاعدة بيانات رأيتها في حياتي بدأت عندما أزال شخص ما كل المفاتيح الخارجية.

ليس لأنه قاس عنق الزجاجة. ولا لأن عمليات الكتابة كانت بطيئة بالفعل. بل لأنه قرأ في مكان ما أن “المفاتيح الخارجية لا تتوسع”. بعد ستة أشهر، كان لديهم 2 مليار سجل يتيم، ونظام فوترة يخصم من مستخدمين محذوفين، وتحليلات بها خطأ بنسبة 40%.

وعندما حاولوا إعادة القيود؟ توقفت قاعدة البيانات عن العمل وهي تحاول التحقق من بيانات موجودة كانت فاسدة بالفعل.

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

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

السؤال ليس ما إذا كانت المفاتيح الخارجية تبطئك. بالطبع تفعل. السؤال هو ما الذي تحصل عليه في المقابل، وهل تحتاجه حقًا.

ما الذي تبادله حقًا

دعني أعطيك مثالًا ملموسًا. أنت تبني نظام مراقبة طقس يحتوي على جداول لمحطات الطقس، وأجهزة الاستشعار، وقراءات المستشعرات، والولايات الأمريكية.

هل تربط كل شيء بمفاتيح خارجية؟ لنفكر فيما يتغير فعليًا وما هي العواقب:

الولايات الأمريكية على الأرجح لن تتغير. وايومنغ لن تُعاد تسميتها قريبًا. لا تحتاج إلى مفتاح خارجي للتحقق من رموز الولايات في كل إدخال عندما تعلم أن البيانات المرجعية ثابتة. هذا عبء لا داعي له.

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

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

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

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

عندما تكون الكتابات السريعة مهمة حقًا

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

لديك بعض الخيارات. يمكنك تغيير مستوى عزل المعاملة من SERIALIZABLE إلى READ COMMITTED، وهو أسرع ولكنه يتخلى عن بعض ضمانات الاتساق. يمكنك تجميع عمليات الالتزام الخاصة بك، وإدراج 1000 صف لكل معاملة بدلاً من واحد تلو الآخر لتوزيع الحمل الزائد للمفتاح الخارجي. أو يمكنك إلغاء التسوية إلى هيكل سجل إلحاقي فقط حيث لا تحاول حتى التحقق من المراجع.

هذا الخيار الثالث ليس غشًا، بالمناسبة. إنه مجرد تصميم مختلف:

CREATE TABLE sensor_log (
id BIGSERIAL PRIMARY KEY,
recorded_at TIMESTAMPTZ NOT NULL,
data JSONB NOT NULL -- { station_id, sensor_id, temp, humidity, ... }
);
CREATE INDEX ON sensor_log USING GIN (data);
CREATE INDEX ON sensor_log (recorded_at);

لا وصلات. لا فحوصات للمفاتيح الخارجية. فقط ألحق البيانات واستعلم حسب النطاق الزمني أو فهرس GIN على كتلة JSONB. هل هذه “أفضل ممارسة”؟ على الأرجح لا بالمعنى الذي تدرسه كتب قواعد البيانات. هل تعمل عندما تقوم بإدراج 50,000 صف في الدقيقة على Raspberry Pi؟ بالتأكيد.

يحدث الانفصال عندما يعامل الناس “أفضل ممارسة” كأمر أخلاقي بدلاً من نمط يعمل بشكل جيد في السيناريوهات الشائعة ولكنه قد لا يناسب سيناريوهاتك.

فخ التطبيع

دورات قواعد البيانات تحب تعليم التطبيع. تجنب التكرار بأي ثمن. الشكل الطبيعي الثالث أو لا شيء.

لذا ينتهي بك الأمر بشيء مثل: الطلباتعناصر الطلبالمنتجاتالأنواعالألوانالمقاسات

ستة وصلات جداول فقط للإجابة على “هل طلبت القميص الأحمر أم الأزرق في عيد الميلاد الماضي؟” ولا سمح الله إن احتجت تضمين اسم المنتج، لأن ذلك يتطلب ثلاث وصلات إضافية في هرم الكتالوج.

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

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

خزّن {"color": "blue", "size": "M"} مباشرة على الطلب. انتهيت.

التعرف على بيانات اللقطة

كيف تعرف متى يجب أن يكون الشيء لقطة؟ اسأل نفسك ما إذا كان سجلاً زمنيًا محددًا:

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

إذا كانت الإجابة “نعم، هذا يسجل لحظة زمنية”، فتوقف عن تطبيعه. ابدأ بأخذ لقطات منه.

الكتل المعتمة

هناك فئة أخرى تتجاوز اللقطات: بيانات لا تستعلم عنها أبدًا. فقط تخزنها وتسترجعها كاملة.

تكوينات نموذج LLM مثل {"model": "gpt-4", "temperature": 0.7, "max_tokens": 2000} ليست شيئًا تستعلم عنه حسب درجة الحرارة. أنت تجلب التكوين بالكامل بواسطة معرف الطلب عندما تحتاجه. حمولات JWT بعد فك التشفير، سجلات طلبات/استجابات API للتصحيح، كائنات تفضيلات المستخدم مع إعدادات السمة وأعلام الإشعارات. هذه كلها كتل معتمة. لا تحتاج إلى تطبيع. لا تحتاج إلى مفاتيح خارجية. احشرها في JSONB وامضِ في حياتك.

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

(لكن كن حذرًا: هذا قد ينعكس بشكل مذهل إذا احتجت لاحقًا إلى الاستعلام عن تلك البيانات. انظر إغراء JSONB لمعرفة متى يخلق هذا الأسلوب كابوسه الخاص.)

الحجم هو السياق

ستسمع الناس يقولون “المفاتيح الخارجية لا تتوسع.” لكن التوسع نسبي تمامًا بالنسبة لعتادك وهندستك المعمارية.

Raspberry Pi يسجل 10,000 قراءة استشعار في الدقيقة على بطاقة microSD؟ هذا توسع عالٍ بشكل شرعي لذلك العتاد. AWS Aurora مع IOPS المخصصة التي تعالج مليارات الصفوف؟ يمكنك استخدام المفاتيح الخارجية خلال ذلك دون عناء.

الحد الصعب الفعلي ليس حول عدد الصفوف أو حجم الكتابة. إنه التقسيم (sharding).

عندما يكون جدول Users على الخادم A وجدول Orders على الخادم B، لا يمكن للمفاتيح الخارجية العمل فعليًا. لا تملك قاعدة البيانات آلية لفرض قيد عبر حدود الشبكة. عند تلك النقطة، تكون بالفعل تدير وظائف خلفية للعثور على السجلات اليتيمة وتنفذ أنماط الاتساق النهائي.

يحدث هذا في تطبيقات SaaS متعددة المستأجرين حيث يحصل كل مستأجر على قاعدة بيانات معزولة خاصة به للامتثال، أو في نشرات إنترنت الأشياء حيث لديك 50,000 جهاز حافة يعمل كل منها محليًا على SQLite. بمجرد الوصول إلى هناك، تصبح المفاتيح الخارجية خارج الطاولة (حرفيًا) بغض النظر عن اعتبارات الأداء.

لكن حتى تصل إلى تلك الحدود المعمارية، ربما لا تحسن مبكرًا لمشاكل Netflix بينما تبني أداة داخلية لـ 10 مستخدمين.

كيف يبدو هذا عمليًا

بدلًا من السؤال “هل يجب استخدام المفاتيح الخارجية”، حاول طرح هذه الأسئلة الثلاثة:

ما الذي ينهار إذا كان هذا المرجع خاطئًا؟ هل هي دعوى قضائية، فواتير تالفة، انتهاك تنظيمي؟ أم مجرد صلة مفقودة ترجع قيمة فارغة في لوحة التحكم التحليلية؟

ما الذي ينهار إذا كان التحقق بطيئًا؟ هل تفقد بيانات زمنية حقيقية لا يمكن تعويضها؟ أم تستغرق استعلاماتك 50 مللي ثانية إضافية فقط؟

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

من هنا، تظهر الأنماط بشكل طبيعي:

المعاملات المالية، جلسات المصادقة، أي شيء يعني فيه تلف البيانات مسؤولية قانونية، ربما يريد مفاتيح خارجية بغض النظر عن عبء الأداء.

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

اللقطات التاريخية مثل الطلبات وسجلات التدقيق، البيانات التي تجلبها دائمًا ككتلة كاملة مثل تفضيلات المستخدم، المخططات التي لا تتحكم فيها مثل حمولات webhook من واجهات برمجة تطبيقات خارجية… غالبًا ما تعمل هذه بشكل أفضل غير معيارية.

لكن لاحظ أني قلت “ربما” و”غالبًا”. لأن السياق مهم، وسياقك يختلف عن سياقي.

أفكار ختامية

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

المشكلة الحقيقية هي عندما يزيل الناس المفاتيح الخارجية بسبب شيء قرأوه عن “النطاق الويب” دون قياس فعلي لما إذا كان لديهم مشكلة في أداء الكتابة أو التفكير فيما يتخلون عنه. ينتهي بك الأمر بتقليد بنية Netflix بشكل أعمى على مشروع جديد يعالج 100 معاملة في اليوم.

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

ما الذي تحسّن أداءه؟

الموارد