-
المساهمات
189 -
تاريخ الانضمام
-
تاريخ آخر زيارة
آخر الزوار
6839 زيارة للملف الشخصي
إنجازات Ola Abbas

عضو نشيط (3/3)
34
السمعة بالموقع
-
عند استخدام قاعدة بيانات علاقية Relational Database، سنحتاج إلى استخدام استعلامات فردية باستخدام لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- لاسترجاع البيانات أو معالجتها مثل استعلامات SELECT أو INSERT أو UPDATE أو DELETE من شيفرة التطبيق مباشرةً، إذ تعمل هذه التعليمات مع جداول قاعدة البيانات الأساسية وتعالجها فورًا. لكن إذا استخدمنا التعليمات أو مجموعة التعليمات نفسها ضمن تطبيقات متعددة يمكنها الوصول إلى قاعدة البيانات نفسها، وتكررت هذه التعليمات عدة مرات فيمكننا في هذه الحالة استخدام الإجراءات المخزَّنة Stored Procedures التي يدعمها MySQL كحال العديد من أنظمة إدارة قواعد البيانات العلاقية الأخرى، حيث تساعد هذه الإجراءات المخزنة في تجميع تعليمة SQL واحدة أو أكثر لإعادة استخدامها باسم مشترك من خلال تغليف منطق العمل المشترك ضمن قاعدة البيانات نفسها، ويمكن استدعاء مثل هذه الإجراءات من التطبيق الذي يصل إلى قاعدة البيانات لاسترجاع البيانات أو معالجتها. تساعدنا الإجراءات المخزنة على إنشاء برامج قابلة لإعادة الاستخدام للمهام الشائعة التي سنستخدمها عبر تطبيقات متعددة، كما توفر طريقة للتحقق من صحة البيانات، أو تقديم طبقة إضافية من أمان الوصول إلى البيانات من خلال تقييد مستخدمي قاعدة البيانات من الوصول إلى الجداول الأساسية مباشرةً وإنشاء استعلامات عشوائية. سنتعلّم في هذا المقال ما هي الإجراءات المخزَّنة وكيفية إنشاء إجراءات مخزنة بسيطة تعيد البيانات، وإجراءات تستخدم كلًا من معاملات الدخل والخرج. مستلزمات العمل يجب أن يكون لدينا حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية RDBMS المستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو مع مستخدم بصلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر معرفة أساسية بتنفيذ استعلامات SELECT لاسترجاع البيانات من قاعدة البيانات ملاحظة: تجدر الإشارة لأنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، ولا تُعَد صيغة الإجراءات المخزنة جزءًا من معيار SQL الرسمي. حيث ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن تُعَد الإجراءات المخزنة خاصة بقاعدة البيانات، وبالتالي قد نجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات فارغة يمكن من خلالها إنشاء جداول توضّح استخدام الإجراءات المخزنة، ويمكن مطالعة القسم التالي للحصول على تفاصيل حول الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية، والتي سنستخدمها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية سنتتصل بخادم MySQL وننشئ قاعدة بيانات تجريبية لاتباع الأمثلة الواردة في هذا المقال، حيث سنستخدم مجموعة سيارات افتراضية، ونخزّن تفاصيل السيارات المملوكة حاليًا مع نوعها وطرازها وسنة بنائها وقيمتها. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نتصل بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات باسم procedures: mysql> CREATE DATABASE procedures; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات procedures من خلال تنفيذ تعليمة USE التالية: $ USE procedures; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، ويمكنك الآن إنشاء جداول تجريبية ضمنها. سيحتوي الجدول cars على بيانات مبسَّطة حول السيارات الموجودة في قاعدة البيانات، حيث سيحتوي على الأعمدة التالية: make: نوع كل سيارة مملوكة، ونمثّل باستخدام نوع البيانات varchar بحد أقصى 100 محرف model: اسم طراز السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 100 محرف year: سنة صنع السيارة باستخدام نوع البيانات int للاحتفاظ بالقيم العددية value: قيمة السيارة باستخدام نوع البيانات decimal بحد أقصى 10 أرقام ورقمين بعد الفاصلة العشرية أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE cars ( mysql> make varchar(100), mysql> model varchar(100), mysql> year int, mysql> value decimal(10, 2) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) سندرج بعض البيانات التجريبية في الجدول cars من خلال تنفيذ عملية INSERT INTO التالية: mysql> INSERT INTO cars mysql> VALUES mysql> ('Porsche', '911 GT3', 2020, 169700), mysql> ('Porsche', 'Cayman GT4', 2018, 118000), mysql> ('Porsche', 'Panamera', 2022, 113200), mysql> ('Porsche', 'Macan', 2019, 27400), mysql> ('Porsche', '718 Boxster', 2017, 48880), mysql> ('Ferrari', '488 GTB', 2015, 254750), mysql> ('Ferrari', 'F8 Tributo', 2019, 375000), mysql> ('Ferrari', 'SF90 Stradale', 2020, 627000), mysql> ('Ferrari', '812 Superfast', 2017, 335300), mysql> ('Ferrari', 'GTC4Lusso', 2016, 268000); تضيف العملية INSERT INTO عشر سيارات رياضية نموذجية للجدول، حيث توجد أربع سيارات من نوع Porsche وخمسة سيارات من نوع Ferrari. يشير الخرج التالي إلى إضافة جميع الصفوف العشرة: الخرج Query OK, 10 rows affected (0.00 sec) Records: 10 Duplicates: 0 Warnings: 0 نحن الآن جاهزون لمتابعة هذا المقال والبدء باستخدام الإجراءات المُخزَّنة في لغة SQL. مقدمة إلى الإجراءات المخزنة Stored Procedures الإجراءات المخزنة في MySQL وفي العديد من أنظمة قواعد البيانات العلاقية الأخرى هي كائنات مُسمَّاة تحتوي على تعليمة واحدة أو أكثر تضعها وتنفّذها قاعدة البيانات عند استدعائها. يمكن للإجراء المخزن حفظ تعليمة مشتركة ضمن برنامج قابل لإعادة الاستخدام مثل استرجاع البيانات من قاعدة البيانات باستخدام المرشّحات Filters المُستخدَمة كثيرًا، حيث يمكنك مثلًا إنشاء إجراء مخزن لاسترجاع عملاء متجر الكتروني قدّموا طلبات خلال عدد معين من الأشهر. يمكن للإجراءات المخزنة أيضًا تمثيل البرامج الشاملة التي تصف منطق الأعمال المعقد للتطبيقات القوية في السيناريوهات الأكثر تعقيدًا. يمكن أن تتضمن مجموعة التعليمات في إجراء مخزَّن تعليمات SQL شائعة مثل استعلامات SELECT أو INSERT التي تعيد البيانات أو تعالجها، ويمكن للإجراءات المخزنة الاستفادة مما يلي: المعاملات المُمرَّرة إلى الإجراء المخزن أو المُعادة منه المتغيرات المُصرَّح عنها لمعالجة البيانات المُسترجَعة من شيفرة الإجراء البرمجية مباشرةً التعليمات الشرطية التي تسمح بتنفيذ أجزاء من شيفرة الإجراء المخزن البرمجية وفق شروط معينة مثل تعليمات IF أو CASE الحلقات مثل WHILE و LOOP و REPEAT لتنفيذ أجزاء من الشيفرة البرمجية عدة مرات تعليمات معالجة الأخطاء مثل إعادة رسائل الخطأ إلى مستخدمي قاعدة البيانات الذين يمكنهم الوصول إلى الإجراء. استدعاءات إجراءات مخزنة أخرى في قاعدة البيانات ملاحظة: تسمح الصيغة الموسَّعة Extensive Syntax التي يدعمها MySQL بكتابة برامج قوية وحل المشكلات المعقدة باستخدام الإجراءات المخزنة مثل التحكم في تدفق البرنامج باستخدام التعليمات الشرطية واستخدام المتغيرات والحلقات ومعالجة الأخطاء المخصصة وغيرها من الاستخدامات ولكن سيغطي هذا المقال فقط الاستخدام الأساسي للإجراءات المخزنة مع تعليمات SQL المُضمَّنة في جسم الإجراء المخزَّن ومعاملات الدخل والخرج، إذ سيكون تنفيذ التعليمات الشرطية واستخدام المتغيرات والحلقات ومعالجة الأخطاء المُخصَّصة خارج نطاق هذا المقال، لذا يمكن مطالعة توثيق MySQL الرسمي لمعرفة المزيد حول الإجراءات المخزنة. إذا استدعينا الإجراء باسمه، فسينفّذه محرّك قاعدة البيانات كما هو مُعرَّف تعليمةً تلو الأخرى. يجب أيضًا أن يكون لدى مستخدم قاعدة البيانات الأذونات المناسبة لتنفيذ الإجراء المُحدَّد، حيث توفر هذه الأذونات المطلوبة طبقة من الأمان، مما يمنع الوصول المباشر إلى قاعدة البيانات مع منح المستخدمين إمكانية الوصول إلى إجراءات فردية مضمونة الأمان لتنفيذها. تُنفَّذ الإجراءات المخزَّنة على خادم قاعدة البيانات مباشرةً مع إجراء جميع العمليات الحسابية محليًا وإعادة النتائج إلى المستخدم المستدعي عند الانتهاء فقط. وإذا أردنا تغيير سلوك الإجراء، فيمكن تحديث الإجراء في قاعدة البيانات، وستلتقط التطبيقات التي تستخدمه الإصدار الجديد تلقائيًا، وسيبدأ جميع المستخدمين باستخدام الشيفرة البرمجية للإجراء الجديد مباشرةً دون الحاجة لتعديل تطبيقاتهم. فيما يلي الهيكل العام لشيفرة SQL المستخدَمة لإنشاء إجراءٍ مخزَّن: mysql> DELIMITER // mysql> CREATE PROCEDURE procedure_name(parameter_1, parameter_2, . . ., parameter_n) mysql> BEGIN mysql> instruction_1; mysql> instruction_2; mysql> . . . mysql> instruction_n; mysql> END // mysql> DELIMITER ; التعليمتان الأولى والأخيرة في مقطع الشيفرة البرمجية السابق هما DELIMITER // و DELIMITER ;، حيث يستخدم MySQL رمز الفاصلة المنقوطة ; لتحديد التعليمات والإشارة إلى بدايتها ونهايتها. إذا نفّذنا تعليمات متعددة في طرفية MySQL مع الفصل بينها بفواصل منقوطة، سيكون التعامل معها كأنها أوامر منفصلة مع تنفيذ كل تعليمة تنفيذًا مستقلًا عن التعليمات الأخرى واحدة تلو الأخرى. يمكن للإجراء المخزَّن أيضًا أن يتضمّن أوامر متعددة ستُنفَّذ تسلسليًا عند استدعائه، مما يشكّل صعوبة عند محاولة إخبار MySQL بإنشاء إجراء جديد، إذ سيرى محرّك قاعدة البيانات علامة الفاصلة المنقوطة في جسم الإجراء المُخزَّن ويعتقد أنه يجب أن يتوقف عن تنفيذ التعليمة، وبالتالي تكون التعليمة المقصودة في هذه الحالة هي الشيفرة البرمجية لإنشاء الإجراء بالكامل، وليس تعليمةً واحدة ضمن الإجراء نفسه، لذا قد يسيء MySQL تفسير ما نقصده. يمكن التغلب على هذا القيد من خلال استخدام الأمر DELIMITER لتغيير هذا المحدِّد مؤقتًا من ; إلى // طوال مدة استدعاء التعليمة CREATE PROCEDURE، ثم ستُمرَّر جميع الفواصل المنقوطة الموجودة في جسم الإجراء المخزَّن إلى الخادم كما هي، ثم يتغير المحدِّد مرة أخرى إلى ; في آخر تعليمة DELIMITER ; بعد الانتهاء من الإجراء بالكامل. يمثّل الاستدعاء CREATE PROCEDURE وبعده اسم الإجراء procedure_name في المثال السابق جوهر الشيفرة البرمجية الخاصة بإنشاء إجراء جديد، ويتبع اسم الإجراء قائمة اختيارية من المعاملات التي سيقبلها الإجراء. الجزء الأخير من الشيفرة البرمجية هو جسم الإجراء المضمَّن ضمن تعليمتي BEGIN و END، ويوجد في الداخل شيفرة الإجراء البرمجية، والتي يمكن أن تحتوي على تعليمة SQL واحدة مثل استعلام SELECT أو شيفرة برمجية أكثر تعقيدًا. ينتهي الأمر END بالرمز //، والذي يُعَد محدِّدًا مؤقتًا عوضًا عن الفاصلة المنقوطة النموذجية. سننشئ في القسم التالي إجراءً مخزنًا بسيطًا بدون معاملات تتضمن استعلامًا واحدًا. إنشاء إجراء مخزن بدون معاملات سننشئ في هذا القسم أول إجراء مخزّن يغلِّف تعليمة SQL واحدة هي التعليمة SELECT لإعادة قائمة السيارات المملوكة المرتبة حسب نوعها وقيمتها بترتيب تنازلي. نبدأ بتنفيذ التعليمة SELECT التي ستستخدمها كما يلي: mysql> SELECT * FROM cars ORDER BY make, value DESC; ستعيد قاعدة البيانات قائمة السيارات من الجدول cars مع ترتيبها حسب نوعها أولًا ثم حسب قيمتها بترتيب تنازلي ضمن نوع السيارة الواحد كما يلي: الخرج +---------+---------------+------+-----------+ | make | model | year | value | +---------+---------------+------+-----------+ | Ferrari | SF90 Stradale | 2020 | 627000.00 | | Ferrari | F8 Tributo | 2019 | 375000.00 | | Ferrari | 812 Superfast | 2017 | 335300.00 | | Ferrari | GTC4Lusso | 2016 | 268000.00 | | Ferrari | 488 GTB | 2015 | 254750.00 | | Porsche | 911 GT3 | 2020 | 169700.00 | | Porsche | Cayman GT4 | 2018 | 118000.00 | | Porsche | Panamera | 2022 | 113200.00 | | Porsche | 718 Boxster | 2017 | 48880.00 | | Porsche | Macan | 2019 | 27400.00 | +---------+---------------+------+-----------+ 10 rows in set (0.00 sec) نلاحظ ظهور سيارة الفيراري الأعلى قيمة في أعلى القائمة، وظهور سيارة البورش الأدنى قيمة في الأسفل. لنفترض استخدام هذا الاستعلام بصورة متكررة في تطبيقات متعددة أو سيستخدمه مستخدمون متعددون ونريد التأكّد من أن الجميع سيستخدمون الطريقة نفسها لترتيب النتائج، لذا يجب إنشاء إجراء مخزن يحفظ تعليمة هذا الاستعلام ضمن إجراء مُسمَّى قابل لإعادة الاستخدام من خلال تنفيذ جزء الشيفرة البرمجية التالي: mysql> DELIMITER // mysql> CREATE PROCEDURE get_all_cars() mysql> BEGIN mysql> SELECT * FROM cars ORDER BY make, value DESC; mysql> END // mysql> DELIMITER ; نلاحظ أن الأمرين الأول DELIMITER // والأخير DELIMITER ; يخبران MySQL بالتوقف عن التعامل مع محرف الفاصلة المنقوطة بوصفه محدِّدًا للتعليمات طوال مدة إنشاء الإجراء كما هو موضّح في القسم السابق. يتبع أمر SQL الذي هو CREATE PROCEDURE اسم الإجراء get_all_cars الذي يمكن تعريفه لوصف ما يفعله الإجراء، ثم يوجد زوج من الأقواس () يمكننا إضافة معاملات ضمنه، ولكن لا يستخدم هذا الإجراء معاملات في مثالنا، لذا ستكون الأقواس فارغة، ثم تُكتَب تعليمة SELECT نفسها المُستخدَمة سابقًا بين الأمرين BEGIN و END اللذين يحددان بداية ونهاية كتلة شيفرة الإجراء البرمجية. ملاحظة: قد يظهر الخطأ ERROR 1044 (42000): Access denied for user 'user'@'localhost' to database 'procedures' عند تنفيذ الأمر CREATE PROCEDURE بناءً على أذونات مستخدم MySQL الخاص بنا. يمكن منح الأذونات اللازمة لإنشاء وتنفيذ الإجراءات المخزنة للمستخدم من خلال تسجيل الدخول إلى MySQL كمستخدم جذر وتنفيذ الأوامر التالية وتغيير اسم مستخدم MySQL والمضيف حسب الحاجة: mysql> GRANT CREATE ROUTINE, ALTER ROUTINE, EXECUTE on *.* TO 'user'@'localhost'; mysql> FLUSH PRIVILEGES; نحدّث أذونات المستخدم، ثم نسجّل الخروج كمستخدم جذر، ونسجّل الدخول مرة أخرى كمستخدم عادي، ثم نعيد تشغيل تعليمة CREATE PROCEDURE. يمكن معرفة المزيد حول تطبيق الأذونات الخاصة بالإجراءات المخزنة لمستخدمي قاعدة البيانات في توثيق MySQL الرسمي الخاص بصلاحيات MySQL والبرامج المُخزَّنة. ستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 0 rows affected (0.02 sec) أصبح الإجراء get_all_cars الآن محفوظًا في قاعدة البيانات، وستُنفَّذ التعليمة المحفوظة كما هي عند استدعائه. يمكن تنفيذ الإجراءات المخزَّنة المحفوظة من خلال استخدام أمر SQL الذي هو CALL متبوعًا باسم الإجراء. نجرّب الآن تشغيل الإجراء الذي أنشأناه كما يلي: mysql> CALL get_all_cars; نحتاج اسم الإجراء get_all_cars فقط لاستخدام هذا الإجراء، إذ لم تَعُد بحاجة إلى كتابة أيّ جزء من تعليمة SELECT التي استخدمناها سابقًا يدويًا، وستعرض قاعدة البيانات النتائج مثل خرج التعليمة SELECT التي نفّذتها سابقًا كما يلي: الخرج +---------+---------------+------+-----------+ | make | model | year | value | +---------+---------------+------+-----------+ | Ferrari | SF90 Stradale | 2020 | 627000.00 | | Ferrari | F8 Tributo | 2019 | 375000.00 | | Ferrari | 812 Superfast | 2017 | 335300.00 | | Ferrari | GTC4Lusso | 2016 | 268000.00 | | Ferrari | 488 GTB | 2015 | 254750.00 | | Porsche | 911 GT3 | 2020 | 169700.00 | | Porsche | Cayman GT4 | 2018 | 118000.00 | | Porsche | Panamera | 2022 | 113200.00 | | Porsche | 718 Boxster | 2017 | 48880.00 | | Porsche | Macan | 2019 | 27400.00 | +---------+---------------+------+-----------+ 10 rows in set (0.00 sec) Query OK, 0 rows affected (0.00 sec) نجحنا في إنشاء إجراء مخزَّن بدون معاملات، حيث يعيد هذا الإجراء جميع السيارات من الجدول cars مرتبةً بطريقة معينة، ويمكن استخدام هذا الإجراء في تطبيقات متعددة. سننشئ في القسم التالي إجراء يقبل المعاملات لتغيير سلوك الإجراء وفقًا لدخل المستخدم. إنشاء إجراء مخزن مع معامل دخل سنضمِّن في هذا القسم معاملات دخل في تعريف الإجراء المخزَّن للسماح للمستخدمين الذين ينفّذون الإجراء بتمرير البيانات إليه، فمثلًا يمكن للمستخدمين توفير مرشّحات للاستعلام. يسترجع الإجراء المخزن get_all_cars الذي أنشأناه مسبقًا جميع السيارات من الجدول cars المصنعة في جميع سنوات التصنيع، ولننشئ الآن إجراء آخر للعثور على السيارات المُصنَّعة في سنة معينة، حيث سنعرِّف معاملًا في تعريف الإجراء من خلال تشغيل الشيفرة البرمجية التالية: mysql> DELIMITER // mysql> CREATE PROCEDURE get_cars_by_year( mysql> IN year_filter int mysql> ) mysql> BEGIN mysql> SELECT * FROM cars WHERE year = year_filter ORDER BY make, value DESC; mysql> END // mysql> DELIMITER ; توجد العديد من التغييرات على شيفرة إنشاء الإجراء مقارنة بالشيفرة المستخدمة في القسم السابق، حيث تغيّر الاسم ليكون get_cars_by_year ليمثل عمل الإجراء، وهو استرجاع السيارات بناءً على سنة إصدارها. كما أصبحت الأقواس الفارغة سابقًا محتوية على تعريف معامل واحد هو IN year_filter int، حيث تخبر الكلمة المفتاحية IN قاعدة البيانات بأن المستخدم المستدعِي سيمرّر المعامل إلى الإجراء. يُعَد year_filter اسمًا عشوائيًا للمعامل، حيث سنستخدمه للإشارة إلى المعامل في شيفرة الإجراء البرمجية، و int هو نوع البيانات، حيث نمثّل سنة التصنيع بقيمة عددية. يظهر المعامل year_filter المُعرَّف بعد اسم الإجراء في تعليمة SELECT ضمن التعليمة WHERE year = year_filter، مما يؤدي إلى ترشيح الجدول cars وفقًا لسنة التصنيع، وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 0 rows affected (0.02 sec) نفّذ الإجراء بدون تمرير أي معاملات إليه كما فعلنا سابقًا: mysql> CALL get_cars_by_year; وستعيد قاعدة بيانات MySQL رسالة الخطأ التالية: رسالة خطأ ERROR 1318 (42000): Incorrect number of arguments for PROCEDURE procedures.get_cars_by_year; expected 1, got 0 يتوقع الإجراء المخزن هذه المرة توفير معامل له، ولكن لم نقدّم له أيّ معامل، لذا يمكننا استدعاء إجراء مخزَّن مع معاملات من خلال توفير قيم المعاملات بين قوسين بنفس الترتيب الذي يتوقعه الإجراء. سننفّذ الإجراء التالي لاسترجاع السيارات المصنعة في عام 2017: mysql> CALL get_cars_by_year(2017); سيُنفَّذ الإجراء المستدعَى الآن تنفيذًا صحيحًا ويعيد قائمة السيارات من عام 2017، وسينتج الخرج التالي: الخرج +---------+---------------+------+-----------+ | make | model | year | value | +---------+---------------+------+-----------+ | Ferrari | 812 Superfast | 2017 | 335300.00 | | Porsche | 718 Boxster | 2017 | 48880.00 | +---------+---------------+------+-----------+ 2 rows in set (0.00 sec) Query OK, 0 rows affected (0.00 sec) تعلّمنا في المثال السابق كيفية تمرير معاملات الدخل إلى الإجراءات المخزنة واستخدامها في الاستعلامات ضمن الإجراء لتوفير خيارات الترشيح، وسنستخدم في القسم التالي معاملات الخرج لإنشاء إجراءات تعيد قيمًا مختلفة متعددة في تنفيذ واحد. إنشاء إجراء مخزن مع معاملات دخل وخرج في الإجراءات المخزَّنة التي أنشأناها في المثالين السابقين استدعينا تعليمة SELECT للحصول على مجموعة نتائج، ولكن قد نحتاج في بعض الحالات إلى إجراء مخزّن يعيد قيمًا مختلفة متعددة مع بعضها البعض بدل إعادة مجموعة نتائج واحدة لاستعلام فردي. لنفترض أننا تريد إنشاء إجراء يوفّر معلومات عن السيارات الصادرة في سنة معينة بما في ذلك كمية السيارات في المجموعة وقيمتها السوقية -الحد الأدنى والحد الأقصى والمتوسط- من خلال استخدام معاملات OUT عند إنشاء إجراء مخزّن جديد. تحتوي معاملات OUT مثل معاملات IN على أسماء وأنواع بيانات مرتبطة بها، ولكن يمكن ملء هذه المعاملات بالبيانات باستخدام الإجراء المخزن بدل تمرير البيانات إلى الإجراء المخزن لإعادة القيم إلى المستخدم المستدعِي. لننشئ الآن الإجراء get_car_stats_by_year التالي الذي سيعيد بيانات موجزة عن السيارات من سنة إنتاج معينة باستخدام معاملات خرج: mysql> DELIMITER // mysql> CREATE PROCEDURE get_car_stats_by_year( mysql> IN year_filter int, mysql> OUT cars_number int, mysql> OUT min_value decimal(10, 2), mysql> OUT avg_value decimal(10, 2), mysql> OUT max_value decimal(10, 2) mysql> ) mysql> BEGIN mysql> SELECT COUNT(*), MIN(value), AVG(value), MAX(value) mysql> INTO cars_number, min_value, avg_value, max_value mysql> FROM cars mysql> WHERE year = year_filter ORDER BY make, value DESC; mysql> END // mysql> DELIMITER ; استخدمنا معامل IN الذي هو year_filter لترشيح السيارات حسب سنة الإصدار، وعرّفنا أربعة معاملات OUT ضمن كتلة الأقواس. نمثّل المعامل cars_number بنوع البيانات int وسنستخدمه لإعادة عدد السيارات في المجموعة، وتمثّل المعاملات min_value و avg_value و max_value القيمة السوقية وتُعرَّف باستخدام نوع البيانات decimal(10, 2) مثل العمود value في الجدول cars، وتُستخدَم هذه المعاملات لإعادة معلومات حول أرخص وأغلى السيارات من المجموعة، بالإضافة إلى متوسط أسعار جميع السيارات المطابقة. تستعلم التعليمة SELECT عن أربع قيم من الجدول cars باستخدام دوال SQL الرياضية وهي: COUNT للحصول على العدد الإجمالي للسيارات، و MIN و AVG و MAX للحصول على القيمة الدنيا والمتوسط والقيمة العليا من العمود value. يمكن مطالعة مقال كيفية استخدام التعابير الرياضية والدوال التجميعية في لغة SQL لمعرفة المزيد حول استخدام الدوال الرياضية في لغة SQL. يمكننا إخبار قاعدة البيانات بأننا نريد تخزين نتائج هذا الاستعلام في معاملات الخرج للإجراء المخزَّن من خلال تقديم كلمة مفتاحية جديدة هي INTO، ونضع بعدها أسماء أربعة معاملات إجراء تقابل البيانات المُسترجَعة، وبالتالي سيحفظ MySQL قيمة COUNT(*) في المعامل cars_number، ونتيجة MIN(value) في المعامل min_value ...إلخ. تؤكد قاعدة البيانات إنشاء الإجراء بنجاح كما يلي: الخرج Query OK, 0 rows affected (0.02 sec) لنشغّل الآن الإجراء الجديد من خلال تنفيذ الأمر التالي: mysql> CALL get_car_stats_by_year(2017, @number, @min, @avg, @max); تبدأ المعاملات الأربعة الجديدة بالإشارة @، وهي أسماء متغيرات محلية في طرفية MySQL يمكنك استخدامها لتخزين البيانات مؤقتًا، وإذا مرّرنا هذه المعاملات إلى الإجراء المخزَّن الذي أنشأناه، فسيدرج الإجراءُ قيمًا في هذه المتغيرات. وستستجيب قاعدة البيانات بالخرج التالي: الخرج Query OK, 1 row affected (0.00 sec) يختلف هذا الخرج عن السلوك السابق، حيث كانت النتائج تُعرَض على الشاشة مباشرةً، لأن نتائج الإجراء المخزَّن محفوظة في معاملات الخرج دون إعادتها كنتيجة للاستعلام، ولكن يمكننا الوصول إلى النتائج من خلال استخدام التعليمة SELECT مباشرةً في صدفة MySQL كما يلي: mysql> SELECT @number, @min, @avg, @max; نحدّد قيمًا من المتغيرات المحلية باستخدام الاستعلام السابق، ولا نستدعي الإجراء مرة أخرى، ويحفظ الإجراء المخزَّن نتائجه في تلك المتغيرات، وستبقى البيانات متاحة حتى قطع الاتصال بالصدفة. ملاحظة: يمكن مطالعة على قسم المتغيرات التي يعرِّفها المستخدم في توثيق MySQL الرسمي لمعرفة المزيد حول استخدام هذه المتغيرات. ستختلف طرق الوصول إلى البيانات المُعادة من الإجراءات المخزنة في لغات البرمجة وأطر العمل المختلفة عند استخدامها في تطوير التطبيقات، لذا يتوجب الاطلاع على توثيق اللغة وإطار العمل لمعرفة الطريقة المناسبة. يعرض الخرج قيم المتغيرات التي استعلمنا عنها كما يلي: الخرج +---------+----------+-----------+-----------+ | @number | @min | @avg | @max | +---------+----------+-----------+-----------+ | 2 | 48880.00 | 192090.00 | 335300.00 | +---------+----------+-----------+-----------+ 1 row in set (0.00 sec) تتوافق القيم مع عدد السيارات المُصنَّعة في عام 2017، والقيمة السوقية الدنيا والمتوسطة والعليا للسيارات في هذه السنة من الإنتاج. تعلّمنا في المثال السابق كيفية استخدام معاملات الخرج لإعادة قيم مختلفة متعددة من الإجراء المخزن لاستخدامها لاحقًا، وسنتعلّم في القسم التالي كيفية إزالة الإجراءات التي أنشأناها. إزالة الإجراءات المخزنة سنزيل في هذا القسم الإجراءات المخزنة الموجودة في قاعدة البيانات، فقد لا تكون هناك حاجة إلى الإجراء الذي أنشأناه في بعض الأحيان، أو قد نرغب في تغيير طريقة عمل الإجراء، حيث لا يسمح MySQL بتغيير تعريف الإجراء بعد إنشائه، فالطريقة الوحيدة لذلك هي إزالة الإجراء أولًا وإعادة إنشائه مرة أخرى مع التغييرات المطلوبة. لنحذف الآن الإجراء الأخير get_car_stats_by_year باستخدام التعليمة DROP PROCEDURE كما يلي: mysql> DROP PROCEDURE get_car_stats_by_year; وستؤكد قاعدة البيانات حذف الإجراء برسالة النجاح التالية: الخرج Query OK, 0 rows affected (0.02 sec) يمكن التحقق من حذف الإجراء من خلال محاولة استدعائه باستخدام الأمر التالي: mysql> CALL get_car_stats_by_year(2017, @number, @min, @avg, @max); سنرى رسالة خطأ تفيد بأن الإجراء غير موجود في قاعدة البيانات كما يلي: رسالة خطأ ERROR 1305 (42000): PROCEDURE procedures.get_car_stats_by_year does not exist تعلمنا في هذا القسم كيفية حذف الإجراءات المخزنة الموجودة في قاعدة البيانات. الخلاصة تعلمنا في هذا المقال ما هي الإجراءات المخزنة وأنواعها المختلفة وكيفية استخدامها في MySQL لحفظ البيانات القابلة لإعادة الاستخدام في إجراءات مسمَّاة وتنفيذها لاحقًا، يمكن استخدام الإجراءات المخزنة لإنشاء برامج قابلة لإعادة الاستخدام، وتوحيد طرق الوصول إلى البيانات عبر تطبيقات متعددة، بالإضافة إلى تنفيذ سلوكيات معقدة تتجاوز الإمكانيات التي توفرها استعلامات SQL الفردية. غطى هذا المقال فقط أساسيات استخدام الإجراءات المخزنة، لذا لمزيد من المعلومات ننصح بالاطلاع على توثيق MySQL للإجراءات المخزنة لمعرفة مزيد من التفاصيل. ترجمة -وبتصرف- للمقال How To Use Stored Procedures in MySQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: استخدام المفاتيح الرئيسية Primary Keys في لغة SQL الاستعلامات الفرعية والإجراءات في SQL جلب الاستعلامات عبر SELECT في SQL حذف الجداول وقواعد البيانات في SQL
-
تتميز قواعد البيانات العلاقية Relational Databases بكونها تهيكل البيانات ضمن بنية منظّمة، فهي تستخدم جداول ذات أعمدة ثابتة وتتبع أنواع بيانات مُعرَّفة بدقة وتضمن بأن جميع الصفوف لها الشكل نفسه. ومن المهم أن نكون قادرين في هذه البينة على العثور على الصفوف في الجداول والإشارة إليها دون التباس عند تخزين هذه البيانات ضمن صفوف الجداول. ويمكننا تحقيق ذلك في لغة الاستعلام البنيوية SQL باستخدام المفاتيح الرئيسية Primary Keys، والتي تعمل كمعرّفات مميزة للصفوف في جداول قاعدة البيانات العلاقية. سنتعرّف في هذا المقال على مفهوم المفاتيح الرئيسية، وكيفية استخدام أنواعها المختلفة لتحديد الصفوف الفريدة في جداول قاعدة البيانات، وسننشئ مفاتيح رئيسية من أعمدة فردية وأعمدة متعددة، كما سننشئ مفاتيح تسلسلية مع زيادة تلقائية باستخدام بعض البيانات التجريبية النموذجية. مستلزمات العمل يجب أن يكون لدينا حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- مستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو مع مستخدم بصلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر معرفة أساسية بتنفيذ استعلامات SELECT كما هو موضّح في مقال كيفية الاستعلام عن السجلات من الجداول في SQL ملاحظة: تجدر الإشارة لأنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر في هذا المقال بنجاح مع معظم هذه الأنظمة، تُعَد المفاتيح الرئيسية جزءًا من معيار SQL، ولكن هناك بعض الميزات خاصة بقاعدة البيانات، لذا قد نجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات فارغة يمكن من خلالها إنشاء جداول توضّح استخدام المفاتيح الرئيسية كما سنشرح بالتفصيل في الفقرات التالية. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل في هذا القسم بخادم MySQL وننشئ قاعدة بيانات تجريبية. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نتصل بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بك مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم primary_keys: mysql> CREATE DATABASE primary_keys; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات primary_keys من خلال تنفيذ تعليمة USE التالية: $ USE primary_keys; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، ويمكن الآن إنشاء جداول تجريبية ضمنها، وبذلك أصبحنا جاهزًا لمتابعة هذا المقال والبدء في العمل مع المفاتيح الرئيسية. مقدمة إلى المفاتيح الرئيسية Primary Keys تُخزَّن البيانات في قاعدة البيانات العلاقية في جداول ذات بنية موحَّدة ومحدَّدة من الصفوف الفردية، ويوضّح تعريف الجدول الأعمدة الموجودة فيه وأنواع البيانات التي يمكن حفظها في الأعمدة الفردية، وهذا كافي لتخزين المعلومات في قاعدة البيانات والعثور عليها لاحقًا باستخدام معايير ترشيح مختلفة باستخدام تعليمة WHERE، ولكن لا تضمن هذه البنية إمكانية العثور على أي صف بعينه دون التباس مع صفوف أخرى. لنفترض أن لدينا قاعدة بيانات لجميع السيارات المسجَّلة المسموح لها بالقيادة على الطرق العامة، حيث ستحتوي قاعدة البيانات على معلومات مثل نوع السيارة وطرازها وسنة تصنيعها ولون طلائها، ولكن إذا بحثنا عن سيارة شيفروليه كامارو Chevrolet Camaro حمراء اللون ومصنوعة في عام 2007، فيمكن العثور على أكثر من سيارة، إذ سيبيع المصنعون سيارات متماثلة لعملاء متعددين، لذا تحتوي السيارات المُسجَّلة على أرقام لوحات ترخيص تحدّد كل سيارة فريدة. إذا بحثنا عن سيارة تحمل لوحة الترخيص OFP857، فيمكن التأكد من أن هذا المعيار سيجد سيارة واحدة فقط، لأن أرقام اللوحات تحدد السيارات المسجلة بطريقة فريدة قانونيًا، ويسمى هذا الجزء من البيانات في قاعدة البيانات العلاقية بالمفتاح الرئيسي Primary Key. المفاتيح الرئيسية هي معرّفات فريدة موجودة في عمود واحد أو مجموعة من الأعمدة يمكنها تحديد كل صف في جدول قاعدة البيانات دون التباس. ومن أبرز خصائص المفاتيح الرئيسية نذكر: يجب أن يستخدم المفتاح الرئيسي قيمًا فريدة، وإذا تكوّن المفتاح الرئيسي من أكثر من عمود واحد، فيجب أن تكون مجموعة القيم في هذه الأعمدة فريدة في الجدول بأكمله، إذ لا يمكن أن يظهر المفتاح الرئيسي أكثر من مرة لأنه مخصّص لتحديد كل صف بطريقة فريدة يجب ألا يحتوي المفتاح الرئيسي على قيم NULL يمكن لكل جدول في قاعدة البيانات استخدام مفتاح رئيسي واحد فقط يفرض محرّك قاعدة البيانات هذه القواعد، لذا يمكنك الوثوق بصحة هذه الخاصيات عند تعريف المفتاح الرئيسي لجدول ما، ويجب أن تضع في بالنا محتوى البيانات وما الذي يمكن اختياره منها لتمثيل المفتاح الرئيسي بشكل مناسب. المفاتيح الطبيعية Natural keys هي معرّفات موجودة مسبقًا في مجموعة البيانات، والمفاتيح البديلة Surrogate Keys هي معرّفات اصطناعية، ويمكن مطالعة مقال فهم قيود SQL لمزيد من التفاصيل. تحتوي بعض هياكل البيانات على مفاتيح رئيسية تظهر في مجموعة البيانات طبيعيًا مثل أرقام لوحات الترخيص في قاعدة بيانات السيارات أو أرقام الضمان الاجتماعي في دليل المواطنين، ولا تكون هذه المعرّفات مؤلَّفة من قيمة واحدة بل مؤلفة من زوج أو مجموعة من عدة قيم في بعض الأحيان، فمثلًا لا يمكن تحديد منزل فريد باستخدام اسم الشارع أو رقم الشارع فقط في دليل مدينة محلية للمنازل، إذ يمكن أن يكون هناك عدة منازل في شارع واحد، ويمكن أن يظهر الرقم نفسه في شوارع متعددة، ولكن يمكن افتراض أن كلًا من اسم الشارع ورقمه هو معرّف منزل فريد. تسمّى هذه المعرّفات بالمفاتيح الطبيعية. لكن لا يمكن في بعض الأحيان تمييز البيانات تمييزًا فريدًا باستخدام قيم عمود واحد أو مجموعة فرعية صغيرة من الأعمدة، وبالتالي يجب إنشاء مفاتيح رئيسية اصطناعية باستخدام تسلسل من الأرقام مثلًا أو معرّفات مُولَّدة عشوائيًا مثل معرّفات UUID، وتسمّى هذه المفاتيح بالمفاتيح البديلة. سننشئ في الأقسام التالية مفاتيح طبيعية بناءً على عمود واحد أو أعمدة متعددة، كما سنولّد مفاتيح بديلة للجداول التي لا يكون المفتاح الطبيعي خيارًا فيها. إنشاء مفتاح رئيسي من عمود واحد تحتوي مجموعة البيانات على عمود واحد طبيعيًا في العديد من الحالات، ويمكن استخدام هذا العمود لتحديد الصفوف في الجدول تحديدًا فريدًا، وبالتالي يمكن إنشاء مفتاح طبيعي لوصف هذه البيانات. لنفترض أن لدينا الجدول التالي باتباع المثال السابق لقاعدة بيانات السيارات المُسجَّلة: جدول بسيط +---------------+-----------+------------+-------+------+ | license_plate | brand | model | color | year | +---------------+-----------+------------+-------+------+ | ABC123 | Ford | Mustang | Red | 2018 | | CES214 | Ford | Mustang | Red | 2018 | | DEF456 | Chevrolet | Camaro | Blue | 2016 | | GHI789 | Dodge | Challenger | Black | 2014 | +---------------+-----------+------------+-------+------+ يمثّل الصف الأول والثاني سيارة فورد موستانج Ford Mustang حمراء مصنوعة في عام 2018، ولن تتمكّن من تحديد السيارة بطريقة فريدة باستخدام نوع السيارة وطرازها فقط، لذا تختلف لوحة الترخيص لكل سيارة في كلتا الحالتين، ممّا يوفّر معرّفًا فريدًا جيدًا لكل صف في الجدول. يُعَد رقم لوحة الترخيص جزءًا من البيانات، لذا يؤدي استخدامه بوصفه مفتاحًا رئيسيًا إلى إنشاء مفتاح طبيعي. إذا أنشأنا الجدول دون استخدام مفتاح رئيسي في العمود license_plate، فستخاطر بظهور لوحة مكررة أو فارغة في مجموعة البيانات لاحقًا. لننشئ الآن جدولًا يشبه الجدول السابق مع استخدام العمود license_plate بوصفه مفتاحًا رئيسيًا مع الأعمدة التالية: license_plate: رقم لوحة الترخيص، ونمثله باستخدام نوع البيانات varchar brand: نوع السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا model: طراز السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا color: لون السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا year: سنة تصنيع السيارة، ونمثّله باستخدام نوع البيانات int لتخزين البيانات العددي. ولننشئ الآن الجدول cars من خلال تنفيذ تعليمة SQL التالية: mysql> CREATE TABLE cars ( mysql> license_plate varchar(8) PRIMARY KEY, mysql> brand varchar(50), mysql> model varchar(50), mysql> color varchar(20), mysql> year int mysql> ); تكون تعليمة PRIMARY KEY بعد تعريف نوع بيانات license_plate، حيث يمكنك استخدام الصيغة المبسَّطة لإنشاء المفتاح وكتابة PRIMARY KEY في تعريف العمود عند التعامل مع المفاتيح الرئيسية المستندة إلى أعمدة مفردة. إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول ببعض الصفوف التجريبية المعروضة في المثال السابق من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO cars VALUES mysql> ('ABC123', 'Ford', 'Mustang', 'Red', 2018), mysql> ('CES214', 'Ford', 'Mustang', 'Red', 2018), mysql> ('DEF456', 'Chevrolet', 'Camaro', 'Blue', 2016), mysql> ('GHI789', 'Dodge', 'Challenger', 'Black', 2014); وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 4 rows affected (0.010 sec) Records: 4 Duplicates: 0 Warnings: 0 يمكن الآن التحقق من أن الجدول الذي أنشأناه يحتوي على البيانات والتنسيق المتوقع باستخدام تعليمة SELECT التالية: mysql> SELECT * FROM cars; وسيظهر الخرج جدولًا مشابهًا للجدول الموجود في بداية هذا القسم: الخرج +---------------+-----------+------------+-------+------+ | license_plate | brand | model | color | year | +---------------+-----------+------------+-------+------+ | ABC123 | Ford | Mustang | Red | 2018 | | CES214 | Ford | Mustang | Red | 2018 | | DEF456 | Chevrolet | Camaro | Blue | 2016 | | GHI789 | Dodge | Challenger | Black | 2014 | +---------------+-----------+------------+-------+------+ يمكن بعد ذلك التحقق من ضمان قواعد المفتاح الرئيسي باستخدام محرّك قاعدة البيانات، لذا نجرّب إدخال سيارة لها رقم لوحة مكرّر من خلال تنفيذ الأمر التالي: mysql> INSERT INTO cars VALUES ('DEF456', 'Jeep', 'Wrangler', 'Yellow', 2019); وسيستجيب MySQL برسالة الخطأ التالية التي تفيد بأن لوحة الترخيص DEF456 تمثّل إدخالًا مكررًا للمفتاح الرئيسي: الخرج ERROR 1062 (23000): Duplicate entry 'DEF456' for key 'cars.PRIMARY' ملاحظة: تُطبَّق المفاتيح الرئيسية باستخدام الفهارس الفريدة Unique Indexes وتشترك في العديد من الخاصيات مع الفهارس التي قد ننشئها يدويًا لأعمدة أخرى في الجدول، وتعمل فهارس المفاتيح الرئيسية أيضًا على تحسين أداء الاستعلام في الجدول للعمود الذي عرّفنا الفهرس له. اطّلع على مقال كيفية استخدام الفهارس في SQL لمزيد من المعلومات. يمكن الآن التأكد من عدم السماح باستخدام لوحات ترخيص مكرّرة، ولنتحقق الآن من إمكانية إدخال سيارة لها لوحة ترخيص فارغة كما يلي: mysql> INSERT INTO cars VALUES (NULL, 'Jeep', 'Wrangler', 'Yellow', 2019); وستستجيب قاعدة البيانات برسالة الخطأ التالية: الخرج ERROR 1048 (23000): Column 'license_plate' cannot be null يمكن التأكّد من أن المفتاح الرئيسي license_plate يحدّد كلّ صف في الجدول تحديدًا فريدًا بفضل القاعدتين السابقتين اللتين تفرضهما قاعدة البيانات، وبالتالي إذا استعلمنا في الجدول عن أي لوحة ترخيص، فسنتوقع إعادة صف واحد في كل مرة. نشرح في القسم التالي كيفية استخدام المفاتيح الرئيسية مع أعمدة متعددة. إنشاء مفتاح رئيسي من أعمدة متعددة إن لم يكن عمود واحد كافيًا لتحديد صف فريد في الجدول، فيمكن إنشاء مفاتيح رئيسية تستخدم أكثر من عمودٍ واحد. لنفترض مثلًا وجود سجلٍ للمنازل بحيث لا يكفي اسم الشارع أو رقمه فقط لتحديد أيّ منزل فردي كما يلي: جدول بسيط +-------------------+---------------+-------------------+------+ | street_name | street_number | house_owner | year | +-------------------+---------------+-------------------+------+ | 5th Avenue | 100 | Bob Johnson | 2018 | | Broadway | 1500 | Jane Smith | 2016 | | Central Park West | 100 | John Doe | 2014 | | Central Park West | 200 | Tom Thompson | 2015 | | Lexington Avenue | 5001 | Samantha Davis | 2010 | | Park Avenue | 7000 | Michael Rodriguez | 2012 | +-------------------+---------------+-------------------+------+ يظهر اسم الشارع Central Park West أكثر من مرة في الجدول، ويظهر رقم الشارع 100 أكثر من مرة أيضًا، ولكن لا تظهَر أي أزواج مكررة من أسماء الشوارع وأرقامها، وبالتالي يمكن استخدام زوج من هاتين القيمتين لتحديد كل صف في الجدول تحديدًا فريدًا في الحالة التي لا يمكن فيها لأي عمود بمفرده أن يكون مفتاحًا رئيسيًا. لننشئ الآن جدولًا يشبه الجدول السابق ويحتوي على الأعمدة التالية: street_name: اسم الشارع الذي يقع فيه المنزل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. street_number: رقم شارع المنزل، ونمثّله باستخدام نوع البيانات varchar، ويمكن لهذا العمود تخزين ما يصل إلى 5 محارف، ولا يستخدم نوع البيانات العددي int لأن بعض أرقام الشوارع قد تحتوي على محارف مثل 200B. house_owner: اسم مالك المنزل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. year: العام الذي بُني فيه المنزل، ونمثّله باستخدام نوع البيانات int لتخزين القيم العددية. سيستخدم المفتاح الرئيسي هذه المرة العمودين street_name و street_number بدلًا من استخدام عمود واحد من خلال تنفيذ تعليمة SQL التالية: mysql> CREATE TABLE houses ( mysql> street_name varchar(50), mysql> street_number varchar(5), mysql> house_owner varchar(50), mysql> year int, mysql> PRIMARY KEY(street_name, street_number) mysql> ); تظهر تعليمة PRIMARY KEY بعد تعريفات الأعمدة على عكس المثال السابق، وتليها أقواس تحتوي على اسمي عمودين هما: street_name و street_number. تنشئ هذه الصيغة المفتاح الرئيسي في الجدول houses الذي يمتد إلى عمودين. إذا ظهر الخرج التالي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول بالصفوف التجريبية المعروضة في المثال السابق من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO houses VALUES mysql> ('Central Park West', '100', 'John Doe', 2014), mysql> ('Broadway', '1500', 'Jane Smith', 2016), mysql> ('5th Avenue', '100', 'Bob Johnson', 2018), mysql> ('Lexington Avenue', '5001', 'Samantha Davis', 2010), mysql> ('Park Avenue', '7000', 'Michael Rodriguez', 2012), mysql> ('Central Park West', '200', 'Tom Thompson', 2015); وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 6 rows affected (0.000 sec) Records: 6 Duplicates: 0 Warnings: 0 يمكن الآن التحقق من أن الجدول الذي أنشأناه يحتوي على البيانات والتنسيق المتوقع باستخدام تعليمة SELECT التالية: mysql> SELECT * FROM houses; وسيُظهِر الخرج جدولًا مشابهًا للجدول الموجود في بداية هذا القسم: الخرج +-------------------+---------------+-------------------+------+ | street_name | street_number | house_owner | year | +-------------------+---------------+-------------------+------+ | 5th Avenue | 100 | Bob Johnson | 2018 | | Broadway | 1500 | Jane Smith | 2016 | | Central Park West | 100 | John Doe | 2014 | | Central Park West | 200 | Tom Thompson | 2015 | | Lexington Avenue | 5001 | Samantha Davis | 2010 | | Park Avenue | 7000 | Michael Rodriguez | 2012 | +-------------------+---------------+-------------------+------+ 6 rows in set (0.000 sec) لنتحقق الآن من سماح قاعدة البيانات بإدراج صفوف تحتوي على أسماء وأرقام شوارع مكرَّرة، مع تقييد ظهور عناوين كاملة مكررة في الجدول. لنبدأ أولًا بإضافة منزل آخر في شارع Park Avenue كما يلي: mysql> INSERT INTO houses VALUES ('Park Avenue', '8000', 'Emily Brown', 2011); سيستجيب MySQL برسالة النجاح التالية لأن العنوان 8000 Park Avenue لم يظهر في الجدول سابقًا: الخرج Query OK, 1 row affected (0.010 sec) وستظهر نتيجة مماثلة عند إضافة منزل في العنوان 8000 Main Street مع تكرار رقم الشارع كما يلي: mysql> INSERT INTO houses VALUES ('Main Street', '8000', 'David Jones', 2009); مما سيؤدي إلى إدراج صف جديد بنجاح بسبب عدم تكرار العنوان الكامل: الخرج Query OK, 1 row affected (0.010 sec) نحاول الآن إضافة منزل آخر في العنوان 100 5th Avenue باستخدام تعليمة INSERT التالية: mysql> INSERT INTO houses VALUES ('5th Avenue', '100', 'Josh Gordon', 2008); وستستجيب قاعدة البيانات برسالة الخطأ التالية لإعلامك بوجود إدخال مكرر للمفتاح الرئيسي لزوج القيم 5th Avenue و 100: الخرج ERROR 1062 (23000): Duplicate entry '5th Avenue-100' for key 'houses.PRIMARY' تطبّق قاعدة البيانات قواعد المفتاح الرئيسي بطريقة صحيحة، مع تعريف المفتاح لزوج من الأعمدة، وبالتالي يمكن التأكّد من عدم تكرار العنوان الكامل المكون من اسم الشارع ورقمه في الجدول. أنشأنا في هذا القسم مفتاحًا طبيعيًا مع زوج من الأعمدة لتحديد كل صف في الجدول house بوصفه صفًا فريدًا، ولكن لا يمكن دائمًا استخلاص المفاتيح الرئيسية من مجموعة البيانات، لذا سنستخدم في القسم التالي المفاتيح الرئيسية الاصطناعية التي لا تأتي من البيانات مباشرةً. إنشاء مفتاح رئيسي تسلسلي Sequential Primary Key أنشأنا مفاتيح رئيسية فريدة باستخدام أعمدة موجودة في مجموعات البيانات التجريبية النموذجية، ولكن البيانات ستتكرّر في بعض الحالات، مما يمنع الأعمدة من أن تكون معرّفات فريدة جيدة، لذا يمكن إنشاء مفاتيح رئيسية تسلسلية باستخدام معرّفات مُولَّدة. تسمَّى المفاتيح الرئيسية التي أنشأناها من المعرّفات الاصطناعية بالمفاتيح البديلة Surrogate Keys عندما تتطلب البيانات المتاحة ابتكار معرّفات جديدة لتحديد الصفوف بطريقة فريدة. لنفترض أن لدينا قائمة بأعضاء نادي الكتاب، وهو تجمع غير رسمي يمكن لأي شخص الانضمام إليه دون بطاقة هوية حكومية، وبالتالي هناك احتمال أن ينضم إلى النادي في وقتٍ ما أشخاص يحملون أسماء متطابقة: جدول بسيط +------------+-----------+ | first_name | last_name | +------------+-----------+ | John | Doe | | Jane | Smith | | Bob | Johnson | | Samantha | Davis | | Michael | Rodriguez | | Tom | Thompson | | Sara | Johnson | | David | Jones | | Jane | Smith | | Bob | Johnson | +------------+-----------+ يتكرر الاسمان Bob Johnson و Jane Smith في الجدول، لذا نحتاج إلى استخدام معرّف إضافي للتأكد من هوية كل منهما، ولا يمكن تحديد الصفوف تحديدًا فريدًا في هذا الجدول بأيّ طريقة، ولكن إذا احتفظنا بقائمة أعضاء نادي الكتاب على الورق، فيمكن الاحتفاظ بمعرّفات مساعدة للتمييز بين الأشخاص الذين يحملون الأسماء نفسها في المجموعة. يمكن إجراء شيء مماثل في قاعدة بيانات علاقية باستخدام عمود إضافي يحتوي على معرّفات مُولَّدة بدون معنى لغرض وحيد هو الفصل بين جميع الصفوف في الجدول بطريقة فريدة، ولنسمي هذا العمود بالاسم member_id، ولكن سيكون من الصعب التوصّل إلى مثل هذا المعرّف كلما أردنا إضافة عضو آخر في نادي الكتاب إلى قاعدة البيانات. يمكن حل هذه المشكلة باستخدام الميزة التي يوفرها MySQL، والتي هي ميزة خاصة بالأعمدة الرقمية المتزايدة تلقائيًا، حيث توفّر قاعدة البيانات قيمة العمود بتسلسل متزايد من الأعداد الصحيحة تلقائيًا. لننشئ الآن جدولًا يشبه الجدول السابق، ولكن سنضيف عمودًا إضافيًا متزايدًا تلقائيًا member_id ليكون محتويًا على العدد المُسنَد لكل عضو في النادي تلقائيًا، وسيمثّل هذا العدد المُسنَد تلقائيًا مفتاحًا رئيسيًا للجدول الذي سيحتوي على الأعمدة التالية: member_id: معرّف رقمي متزايد تلقائيًا، ونمثّله باستخدام نوع البيانات int. first_name: الاسم الأول لأعضاء النادي، والذي نمثله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. last_name: يالاسم الأخير لأعضاء النادي، والذي نمثله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. لننشئ هذا الجدول من خلال تنفيذ تعليمة SQL التالية: mysql> CREATE TABLE club_members ( mysql> member_id int AUTO_INCREMENT PRIMARY KEY, mysql> first_name varchar(50), mysql> last_name varchar(50) mysql> ); تظهَر تعليمة PRIMARY KEY بعد تعريف نوع العمود مثل المفتاح الرئيسي لعمود واحد، ولكن تظهَر سمة Attribute إضافية قبلها هي AUTO_INCREMENT التي تخبر MySQL بتوليد قيم تلقائيًا لهذا العمود باستخدام تسلسلٍ متزايد من الأرقام إن لم تكن متوفّرة صراحةً. ملاحظة: تُعَد الخاصية AUTO_INCREMENT لتعريف عمود خاصةً بقاعدة بيانات MySQL، ولكن توفّر قواعد البيانات الأخرى طرقًا مماثلة لتوليد مفاتيح تسلسلية مع اختلاف صيغتها بين المحرّكات، لذا نشجّعك على الرجوع إلى التوثيق الرسمي لنظام إدارة قواعد البيانات العلاقية الخاص بك لمزيد من التفاصيل. إذا ظهر الخرج التالي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول بالصفوف التجريبية النموذجية المعروضة في المثال السابق من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO club_members (first_name, last_name) VALUES mysql> ('John', 'Doe'), mysql> ('Jane', 'Smith'), mysql> ('Bob', 'Johnson'), mysql> ('Samantha', 'Davis'), mysql> ('Michael', 'Rodriguez'), mysql> ('Tom', 'Thompson'), mysql> ('Sara', 'Johnson'), mysql> ('David', 'Jones'), mysql> ('Jane', 'Smith'), mysql> ('Bob', 'Johnson'); تتضمن تعليمة INSERT الآن قائمة بأسماء الأعمدة first_name و first_name، مما يضمن معرفة قاعدة البيانات بأن العمود member_id غير مُدرَج في مجموعة البيانات، لذا يجب أخذ القيمة الافتراضية له بدلًا من ذلك. وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 10 rows affected (0.002 sec) Records: 10 Duplicates: 0 Warnings: 0 نستخدم تعليمة SELECT التالية للتحقق من البيانات الموجودة في الجدول الذي أنشأناه: mysql> SELECT * FROM club_members; وسيعرض الخرج التالي جدولًا مشابهًا للجدول الموجود في بداية هذا القسم: الخرج +-----------+------------+-----------+ | member_id | first_name | last_name | +-----------+------------+-----------+ | 1 | John | Doe | | 2 | Jane | Smith | | 3 | Bob | Johnson | | 4 | Samantha | Davis | | 5 | Michael | Rodriguez | | 6 | Tom | Thompson | | 7 | Sara | Johnson | | 8 | David | Jones | | 9 | Jane | Smith | | 10 | Bob | Johnson | +-----------+------------+-----------+ 10 rows in set (0.000 sec) ولكن سيظهر العمود member_id في النتيجة، والذي يحتوي على تسلسل من الأرقام من 1 إلى 10، وبالتالي أصبح من الممكن التمييز بين الصفوف Jane Smith و Bob Johnson المكرّرة، إذ سيرتبط كل اسم بمعرّف فريد member_id. لنتحقّق الآن مما إذا كانت قاعدة البيانات ستسمح بإضافة بعضو آخر اسمه Tom Thompson إلى قائمة أعضاء النادي كما يلي: mysql> INSERT INTO club_members (first_name, last_name) VALUES ('Tom', 'Thompson'); وسيستجيب MySQL برسالة النجاح التالية: الخرج Query OK, 1 row affected (0.009 sec) ولنتحقق الآن من المعرّف الرقمي الذي أسندَته قاعدة البيانات للإدخال الجديد من خلال تنفيذ استعلام SELECT التالي: mysql> SELECT * FROM club_members; وسيظهر الخرج التالي الذي سيحتوي على صفٍ جديد: الخرج +-----------+------------+-----------+ | member_id | first_name | last_name | +-----------+------------+-----------+ | 1 | John | Doe | | 2 | Jane | Smith | | 3 | Bob | Johnson | | 4 | Samantha | Davis | | 5 | Michael | Rodriguez | | 6 | Tom | Thompson | | 7 | Sara | Johnson | | 8 | David | Jones | | 9 | Jane | Smith | | 10 | Bob | Johnson | | 11 | Tom | Thompson | +-----------+------------+-----------+ 11 rows in set (0.000 sec) أُسنِد الرقم 11 إلى الصف الجديد تلقائيًا في عمود member_id باستخدام ميزة AUTO_INCREMENT في قاعدة البيانات. إذا لم يكن للبيانات التي نعمل عليها مرشَّحين طبيعيين ليكونوا مفاتيح رئيسية، ولا نريد توفير معرّفات اصطناعية في كل مرة نضيف فيها بيانات جديدة إلى قاعدة البيانات، فيمكن الاعتماد على المعرّفات المولَّدة تسلسليًا لتكون مفاتيح رئيسية. الخلاصة تعلّمنا في هذا المقال ما هي المفاتيح الرئيسية وكيفية إنشاء أنواعها الشائعة في MySQL لتحديد الصفوف الفريدة في جداول قاعدة البيانات، حيث بنينا مفاتيح رئيسية طبيعية، وأنشأنا مفاتيح رئيسية تمتد على أعمدة متعددة، واستخدمنا مفاتيح تسلسلية متزايدة تلقائيًا عند عدم وجود مفاتيح طبيعية. يمكن استخدام المفاتيح الرئيسية لتشكيل بنية قاعدة البيانات، مما يضمن إمكانية التعرّف على صفوف البيانات بطريقة فريدة. وضّح هذا المقال أساسيات استخدام المفاتيح الرئيسية فقط، لذا يمكن مطالعة توثيق MySQL للقيود لمزيد من المعلومات، ويمكن أيضًا الاطلاع على مقال فهم قيود SQL ومقال كيفية استخدام القيود في SQL. ترجمة -وبتصرف- للمقال How To Use Primary Keys in SQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: الفهارس متعددة الأعمدة في SQL كيفية استخدام القوادح Triggers في SQL كيفية إنشاء وإدارة الجداول في SQL مفاتيح الجداول
-
سنتعلم في مقال اليوم كيفية حفظ البيانات المحلية بين جلسات اللعب وتحميل هذه البيانات عند الحاجة لها، هذا الموضوع مهم بشكل خاص عندما نريد الاحتفاظ بتقدم اللاعب أو إعدادات اللعبة عبر عدة جلسات لعب، حتى بعد إغلاق اللعبة وإعادة فتحها. كيف نحفظ البيانات المحلية في جودو يعتمد نظام إدخال وإخراج الملفات الخاص بمحرك الألعاب جودو Godot على كائن يسمى FileAccess ويمكنا فتحه من خلال استدعاء التابع open() كما يلي: var file = FileAccess.open("user://myfile.name", File.READ) ملاحظة: يجب تخزين بيانات المستخدم فقط في المسار user://، ويمكننا استخدام المسار res:// عند التشغيل من المحرّر، ولكن يصبح هذا المسار للقراءة فقط عند تصدير مشروعنا. الوسيط الثاني الموجود بعد مسار الملف هو راية الوضع Mode Flag ويمكن أن يكون لها أحد الخيارات التالية: FileAccess.READ: مفتوح للقراءة FileAccess.WRITE: مفتوح للكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه Truncate إذا كان موجودًا مسبقًا FileAccess.READ_WRITE: مفتوح للقراءة والكتابة، ولا يقتطع الملف FileAccess.WRITE_READ: مفتوح للقراءة أو الكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه إذا كان موجودًا مسبقًا تخزين البيانات يمكننا حفظ البيانات باستخدام نوع البيانات المحدّد مثل store_float() و store_string() وغير ذلك، أو باستخدام الدالة store_var() المعمَّمة، والتي ستستخدم التسلسل Serialization المُدمَج في جودو لتشفير بياناتك بما في ذلك البيانات المعقدة مثل الكائنات التي سنتحدث عنها لاحقًا. لنبدأ بمثال بسيط لحفظ أعلى نتيجة للاعب، حيث يمكننا كتابة دالة يمكن استدعاؤها كلما احتجنا إلى حفظ النتيجة: var save_path = "user://score.save" func save_score(): var file = FileAccess.open(save_path, FileAccess.WRITE) file.store_var(highscore) نحفظ النتيجة، ولكن يجب تحميلها عند بدء اللعبة كما يلي: func load_score(): if FileAccess.file_exists(save_path): print("file found") var file = FileAccess.open(save_path, FileAccess.READ) highscore = file.get_var() else: print("file not found") highscore = 0 علينا أن لا ننسى التحقق من وجود الملف قبل محاولة القراءة منه، إذ قد لا يكون موجودًا، وإن كان غير موجود، فيمكننا استخدام قيمة افتراضية. كما يمكن استخدام الدالتين store_var() و get_var() عدة مرات حسب حاجتك مع أيّ عدد من القيم. حفظ الموارد تعمل الطريقة السابقة بنجاح عندما نريد أن نحفظ عددًا معينًا من القيم، ولكن يمكننا حفظ بياناتنا في مورد Resource في الحالات الأكثر تعقيدًا كما يفعل جودو الذي يحفظ جميع موارد البيانات الخاصة به على أنها ملفات .tres مثل Animations و TileSets و Shaders وما إلى ذلك حيث يمكننا تطبيق ذلك أيضًا. يمكن حفظ الموارد وتحميلها باستخدام صنفي جودو ResourceSaver و ResourceLoader. لنفترض مثلًا تخزين جميع بيانات الإحصائيات الخاصة بشخصية لعبتنا في مورد كما يلي: extends Resource class_name PlayerData var level = 1 var experience = 100 var strength = 5 var intelligence = 3 var charisma = 2 يمكننا بعد ذلك الحفظ والتحميل كما يلي: func load_character_data(): if ResourceLoader.exists(save_path): return load(save_path) return null func save_character_data(data): ResourceSaver.save(data, save_path) قد تحتوي الموارد على موارد فرعية، لذا يمكن أيضًا تضمين موارد مخزن اللاعب وغير ذلك. هل يمكن تخزين البيانات في ملف JSON قد يخطر في البال سؤال عن إمكانية استخدام صيغة JSON لحفظ البيانات، ولكن يُوصَى بعدم استخدام JSON مع ملفات الحفظ الخاصة بنا. يدعم جودو صيغة JSON، ولكن لا يُعَد حفظ بيانات اللعبة هدف استخدام JSON التي هي صيغة لتبادل البيانات، والغرض منها هو السماح للأنظمة التي تستخدم صيغ بيانات ولغات مختلفة بتبادل البيانات، لذا ستسبّب صيغة JSON قيودًا سلبية عندما يتعلق الأمر بحفظ بيانات اللعبة. إضافة لذلك لا تدعم JSON العديد من أنواع البيانات، إذ لا يوجد نوع البيانات int مقابل نوع البيانات float مثلًا، لذا يجب إجراء الكثير من عمليات التحويل والتحقق لمحاولة حفظ أو تحميل بياناتنا، ويُعَد ذلك أمرًا مرهقًا ويستغرق وقتًا طويلًا. لا ننصح بتضيع الوقت في محاولة كهذه، إذ يمكننا تخزين كائنات جودو الأصيلة مثل العقد والموارد والمشاهد دون أي جهد باستخدام التسلسل المُدمَج في جودو، مما يعني أننا ستستخدم شيفرة برمجية أقل مع وجود أخطاء أقل، ولا يستخدم جودو صيغة JSON لحفظ المشاهد والموارد. الخاتمة تعلمنا في مقال اليوم أساسيات حفظ واسترجاع البيانات المحلية بين جلسات اللعب، وتجدر الإشارة لأن هذا المقال لا يمثل سوى جزء بسيط ممّا يمكنك إنجازه باستخدام FileAccess، لذا ننصح بالاطلاع على توثيق FileAccess للحصول على القائمة الكاملة لتوابعه. ترجمة -وبتصرّف- للقسم Saving/loading data من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: تعرف على مفهوم Delta في تطوير الألعاب الطريقة الصحيحة للتواصل بين العقد في جودو Godot لغات البرمجة المتاحة في جودو Godot إنشاء شخصيات ثلاثية الأبعاد في جودو Godot
-
شرحنا في المقال السابق مفهوم الفهارس في قاعدة البيانات، ووضحنا أنواعًا مختلفة من الفهارس على قاعدة البيانات، وكانت جميع هذه الفهارس معرًفة باستخدام اسم عمود واحد single column، حيث يتعلق هذا الفهرس بقيم هذا العمود المختار، ولكن تدعم معظم أنظمة قواعد البيانات الفهارس التي تمتد لأكثر من عمود واحد multiple columns، وهذا ما سنوضّحه في هذا المقال، بالإضافة توضيح كيفية سرد وإزالة الفهارس الموجودة مسبقًا. استخدام الفهارس مع أعمدة متعددة توفر الفهارس متعددة الأعمدة طريقةً لتخزين قيم أعمدة متعددة في فهرس واحد، مما يسمح لمحرّك قاعدة البيانات بتنفيذ الاستعلامات بسرعة وكفاءة أكبر باستخدام مجموعة الأعمدة مع بعضها البعض. فالاستعلامات المستخدَمة بصورة متكررة والتي يجب تحسينها للحصول على أداء أفضل تستخدم شروطًا متعددة في تعليمة الترشيح WHERE في أغلب الأحيان، ومن الأمثلة على هذا النوع من الاستعلامات استعلام يطلب من قاعدة البيانات أن تعثر على شخص معين من خلال اسمه الأول والأخير كما يلي: mysql> SELECT * FROM employees WHERE last_name = 'Smith' AND first_name = 'John'; قد تكون الفكرة الأولى لتحسين هذا الاستعلام باستخدام الفهارس هي إنشاء فهرسين، أحدهما في العمود last_name والآخر في العمود first_name، ولكنه ليس الخيار الأفضل لهذه الحالة، حيث فإذا أنشأنا فهرسين منفصلين بهذه الطريقة، فسيعرف MySQL كيفية العثور على جميع الموظفين الذين يحملون اسم Smith مثلًا، وسيعرف أيضًا كيفية العثور على جميع الموظفين الذين يحملون اسم John، ولكنه لن يعرف كيفية العثور على الموظفين الذين يحملون الاسم John Smith. لنوضّح مشكلة وجود فهرسين فرديين من خلال تخيل وجود دليلي هاتف منفصلين، أحدهما مرتب حسب الاسم الأخير والآخر حسب الاسم الأول، ويشبه هذان الدليلان الفهارس التي أنشأناها في المقال السابق في العمودين last_name و first_name على التوالي. يمكنك التعامل مع مشكلة العثور على الاسم John Smith كمستخدمٍ لدليل الهاتف باستخدام ثلاث طرق ممكنة هي: الطريقة الأولى هي استخدم دليل الهاتف المرتب حسب الاسم الأخير للعثور على جميع الأشخاص الذين يحملون الاسم Smith، وتجاهل دليل الهاتف الثاني، ثم يمكن المرور يدويًا على جميع الأشخاص الذين يحملون اسم Smith واحدًا تلو الآخر حتى نجد الاسم John Smith. الطريقة الثانية هي تطبيق الطريقة المعاكسة من خلال استخدام دليل الهاتف المرتب حسب الاسم الأول للعثور على جميع الأشخاص الذين يحملون اسم John، وتجاهل دليل الهاتف الثاني، ثم المرور يدويًا على جميع الأشخاص الذين يحملون اسم John واحدًا تلو الآخر حتى نجد الاسم John Smith. الطريقة الأخيرة هي محاولة استخدام دليلي الهاتف معًا من خلال البحث عن جميع الأشخاص الذين يحملون اسم John وعن جميع الأشخاص الذين يحملون اسم Smith بطريقة منفصلة، وكتابة النتائج المؤقتة، ثم نحاول يدويًا إيجاد تقاطع هاتين المجموعتين الفرعيتين من البيانات بحثًا عن الأشخاص الموجودين في القائمتين الفرديتين. لا تُعَد أي طريقة من الطرق السابقة مثالية، ويوفر MySQL أيضًا خيارات مماثلة عند التعامل مع العديد من الفهارس المنفصلة والاستعلامات التي تطلب أكثر من شرط ترشيح واحد. توجد طريقة أخرى أيضًا تتمثّل باستخدام الفهارس التي تأخذ عدة أعمدة بدلًا من عمود واحد، حيث يمكنك تخيل ذلك كدليل هاتف موضوع ضمن دليل هاتف آخر، إذ سنبحث أولًا عن الاسم الأخير Smith، مما يوجّهنا إلى الدليل الثاني لجميع الأشخاص الذين يحملون اسم Smith بحيث تكون الأسماء مرتبة أبجديًا حسب الاسم الأول، ويمكننا استخدام هذا الدليل للعثور على الاسم John بسرعة. إنشاء فهرس متعدد الأعمدة يمكن إنشاء فهرس متعدد الأعمدة في MySQL للأسماء الأخيرة والأسماء الأولى في الجدول employees من خلال تنفيذ التعليمة التالية: mysql> CREATE INDEX names ON employees(last_name, first_name); تختلف التعليمة CREATE INDEX في هذه الحالة بعض الشيء، حيث ستحتوي على عمودين هما: last_name ثم first_name بين قوسين بعد اسم الجدول employees، مما يؤدي إلى إنشاء فهرس متعدد الأعمدة مع هذين العمودين، ويُعَد ترتيب الأعمدة في تعريف الفهرس مهمًا. تعرض قاعدة البيانات الرسالة التالية التي تؤكّد إنشاء الفهرس بنجاح: الخرج Query OK, 0 rows affected (0.024 sec) Records: 0 Duplicates: 0 Warnings: 0 نستخدم الآن استعلام SELECT للعثور على الصفوف التي يتطابق فيها الاسم الأول مع John والاسم الأخير مع Smith كما يلي: mysql> SELECT * FROM employees WHERE last_name = 'Smith' AND first_name = 'John'; وتكون النتيجة صفًا واحدًا يحتوي على موظف اسمه John Smith: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) نستخدم الآن استعلام مع أمر EXPLAIN التالي للتحقق من استخدام الفهرس: mysql> EXPLAIN SELECT * FROM employees WHERE last_name = 'Smith' AND first_name = 'John'; وستكون طريقة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+------+---------------+-------+---------+-------------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+-------+---------+-------------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | ref | names | names | 406 | const,const | 1 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+-------+---------+-------------+------+----------+-------+ 1 row in set, 1 warning (0.00 sec) في هذه الحالة استخدمت قاعدة البيانات الفهرس names، ومسحت صفًا واحدًا، لذا لم تمر على الجدول أكثر مما تحتاج إليه. يحتوي العمود Extra على العبارة Using index condition التي تعني أن MySQL يمكنه إكمال الترشيح باستخدام الفهرس فقط، حيث يوفّر الترشيح -وفقًا للأسماء الأولى والأخيرة باستخدام الفهرس متعدد الأعمدة الذي يمتد بين هذين العمودين لقاعدة البيانات- طريقةً مباشرة وسريعة للعثور على النتائج المطلوبة. لنشاهد الآن ما سيحدث إذا حاولنا العثور على جميع الموظفين الذين يحملون اسم Smith دون الترشيح وفقًا للاسم الأول مع تعريف الفهرس في العمودين، ولنشغّل الاستعلام المعدَّل التالي: mysql> SELECT * FROM employees WHERE last_name = 'Smith'; وستظهر النتائج التالية: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 20 | Abigail | Smith | FGH890 | 155000 | | 17 | Daniel | Smith | WXY901 | 140000 | | 1 | John | Smith | ABC123 | 60000 | | 5 | Michael | Smith | MNO345 | 80000 | +-------------+------------+-----------+---------------+--------+ 4 rows in set (0.000 sec) نلاحظ وجود أربع موظفين يحملون الاسم الأخير Smith. ننتقل الآن إلى طريقة تنفيذ الاستعلام باستخدام التعليمة التالية: mysql> EXPLAIN SELECT * FROM employees WHERE last_name = 'Smith'; وستكون طريقة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+------+---------------+-------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+-------+---------+-------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | ref | names | names | 203 | const | 4 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+-------+---------+-------+------+----------+-------+ 1 row in set, 1 warning (0.01 sec) نلاحظ إعادة 4 صفوف هذه المرة، حيث يوجد أكثر من موظف يحمل هذا الاسم الأخير، ولكن يوضّح جدول طريقة التنفيذ أن قاعدة البيانات استخدمت الفهرس متعدد الأعمدة names لإجراء هذا الاستعلام، ومسحت 4 صفوف فقط، وهو العدد الدقيق المُعاد. مرّرنا في الاستعلامات السابقة العمود المُستخدَم لترشيح النتائج last_name أولًا في تعليمة CREATE INDEX، وسنرشّح الآن الجدول employees وفق العمود first_name، وهو العمود الثاني في قائمة الأعمدة لهذا الفهرس متعدد الأعمدة، لذا ننفّذ الآن الاستعلام التالي: mysql> SELECT * FROM employees WHERE first_name = 'John'; وسيظهر الخرج التالي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) لننتقل الآن لعرض طريقة تنفيذ الاستعلام كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE first_name = 'John'; وسيظهر الخرج التالي: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | NULL | NULL | NULL | NULL | 20 | 10.00 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) تحتوي النتائج المُعادة على موظف واحد دون استخدام أي فهرس هذه المرة، ومسحَت قاعدة البيانات الجدول بالكامل كما يوضح التعليق Using where في العمود Extra، بالإضافة إلى 20 صفًا ممسوحًا. لم تستخدم قاعدة البيانات الفهرس في هذه الحالة بسبب ترتيب الأعمدة المُمرَّرة إلى التعليمة CREATE INDEX عند إنشاء الفهرس لأول مرة: last_name, first_name، إذ لا يمكن لقاعدة البيانات استخدام الفهرس إلا إذا استخدم الاستعلام العمود الأول أو العمودين الأول والثاني، ولا يمكنها دعم الاستعلامات مع الفهرس عند عدم استخدام العمود الأول من تعريف الفهرس. إذا أنشأنا فهرسًا لأعمدة متعددة، فيمكن لقاعدة البيانات استخدام هذا الفهرس لتسريع الاستعلامات التي تتضمن جميع الأعمدة المفهرسَة أو ذات البادئة المتزايدة اليسارية لجميع الأعمدة المفهرسَة، فمثلًا يمكن استخدام فهرس متعدد الأعمدة يتضمن الأعمدة a و b و c لتسريع الاستعلامات التي تتضمن جميع الأعمدة الثلاثة، والاستعلامات التي تتضمن العمودين الأولين فقط، أو حتى الاستعلامات التي تتضمن العمود الأول فقط، ولكن لن يساعد الفهرس في الاستعلامات التي تتضمن العمود الأخير فقط c أو العمودين الأخيرين b و c. يمكن استخدام فهرس واحد متعدد الأعمدة لتسريع الاستعلامات المختلفة للجدول نفسه من خلال اختيار الأعمدة المُضمَّنة في الفهرس بعناية وترتيبها، فمثلًا إذا افترضنا أن نبحث عن الموظفين باستخدام الاسم الأول والأخير أو الاسم الأخير فقط، فسيضمن الترتيب المُقدَّم للأعمدة في الفهرس names أن الفهرس سيسرّع جميع الاستعلامات ذات الصلة. استخدمنا في هذا القسم فهرسًا متعدد الأعمدة وتعلّمنا ترتيب الأعمدة عند تحديد مثل هذا الفهرس، وسنتعلّم في الفقرات التالية كيفية إدارة الفهارس الموجودة مسبقًا. سرد وإزالة الفهارس الموجودة مسبقًا أنشأنا في الأقسام السابقة فهارس جديدة، بما أن الفهارس لها أسماء وتُعرَّف لجداول معينة، فيمكننا أيضًا سردها ومعالجتها عند الحاجة، حيث يمكن سرد جميع الفهارس التي أنشأناها سابقًا للجدول employees من خلال تنفيذ التعليمة التالية: mysql> SHOW INDEXES FROM employees; وسيكون الخرج مشابهًا لما يلي: الخرج +-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | +-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | employees | 0 | device_serial | 1 | device_serial | A | 20 | NULL | NULL | YES | BTREE | | | YES | NULL | | employees | 1 | salary | 1 | salary | A | 20 | NULL | NULL | YES | BTREE | | | YES | NULL | | employees | 1 | names | 1 | last_name | A | 16 | NULL | NULL | YES | BTREE | | | YES | NULL | | employees | 1 | names | 2 | first_name | A | 20 | NULL | NULL | YES | BTREE | | | YES | NULL | +-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ 4 rows in set (0.01 sec) قد يختلف الخرج بعض الشيء اعتمادًا على إصدار MySQL الخاص بنا، ولكنه سيتضمن جميع الفهارس مع أسمائها والأعمدة المستخدمة لتعريف الفهرس والمعلومات التي تجعله فريدًا وتفاصيل أخرى لتعريف الفهرس. يمكن حذف الفهارس الموجودة مسبقًا من خلال استخدام تعليمة SQL التالية: DROP INDEX فإن لم نعد نرغب في فرض جعل العمود device_serial فريدًا، فلن تكون هناك حاجة إلى الفهرس device_serial بعد الآن، وسننفّذ الأمر التالي لحذفه: mysql> DROP INDEX device_serial ON employees; device_serial هو اسم الفهرس و employees هو الجدول الذي عرّفنا الفهرس له، وستؤكد قاعدة البيانات حذف الفهرس كما يلي: الخرج Query OK, 0 rows affected (0.018 sec) Records: 0 Duplicates: 0 Warnings: 0 قد تتغير أنماط الاستعلامات النموذجية بمرور الوقت، وقد تظهر أنواع استعلامات جديدة في بعض الأحيان، لذا قد نحتاج إلى إعادة تقييم الفهارس التي نستخدمها أو إنشاء فهارس جديدة أو حذف الفهارس غير المستخدمة لتجنب تناقص أداء قاعدة البيانات من خلال تحديثها باستمرار. يمكننا إدارة الفهارس في قاعدة بيانات موجودة مسبقًا باستخدام أوامر CREATE INDEX و DROP INDEX من خلال اتباع أفضل الممارسات لإنشاء الفهارس عندما تصبح ضرورية ومفيدة. الخلاصة تعلّمنا في هذا المقال كيف يمكن تعريف فهارس متعددة الأعمدة وكيف يمكن للفهارس أن تؤثر على الاستعلامات عند استخدام أكثر من عمود واحد في شرط الترشيح وكيفية سرد وإزالة الفهارس الموجودة مسبقًا، وقد ركزنا على أمثلة بسيطة توضح أساسيات استخدام الفهارس فقط، ولكن يمكننا دعم الاستعلامات الأكثر تعقيدًا من خلال الفهارس عند فهم كيفية اختيار MySQL للفهارس المُستخدَمة ومتى يستخدمها، لذا يمكن الرجوع لتوثيق MySQL للفهارس لمزيد من المعلومات. ترجمة -وبتصرف- للجزء الثاني من مقال How To Use Indexes in MySQL لصاحبيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: مقدمة إلى الفهارس Indexes في SQL الفهارس Indexes في SQL إنشاء فهرس CREATE INDEX حذف الفهرس DROP INDEX تعديل الفهرس ALTER INDEX
-
يمكن استخدام قواعد البيانات العلاقية Relational Databases للعمل مع بيانات من جميع الأحجام بما في ذلك قواعد البيانات الكبيرة التي تحتوي على ملايين الصفوف، وتوفّر لنا لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- طريقة موجزة ومباشرة للعثور على صفوف معينة في جداول قاعدة البيانات وفق معايير محددة، ولكن مع تزايد الحجم سيصبح تحديد موقع صفوف معينة في قواعد البيانات أكثر صعوبة ويشبه البحث عن إبرة في كومة قش! تصعّب قدرة قواعد البيانات على قبول مجموعة واسعة من شروط الاستعلام على محرّك قاعدة البيانات توقّع الاستعلامات الأكثر شيوعًا، إذ يجب أن يكون المحرّك مستعدًا لتحديد موقع الصفوف بكفاءة في جداول قاعدة البيانات بغض النظر عن حجمها، ولكن بطبيعة الحالة سيسوء أداء البحث مع زيادة حجم البيانات وسيصعب العثور على النتائج التي تتطابق مع الاستعلام بسرعة. في هذه الحالة يمكن لمسؤول قواعد البيانات استخدام مفهوم الفهارس Indexes لمساعدة محرّك قاعدة البيانات على تسريع البحث وتحسين أدائه، حيث سنتعلم في هذا المقال مفهوم الفهارس وكيفية إنشائها للاستفادة منها في الاستعلام من قاعدة البيانات. مستلزمات العمل يجب أن يكون لدينا حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- مستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذي صلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة التالية من المقال معرفة أساسية بتنفيذ استعلامات SELECT لاسترجاع البيانات من قاعدة البيانات كما هو موضّح في مقال الاستعلام عن السجلات من الجداول في SQL ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن الفهارس ليست جزءًا من صيغة SQL المعيارية، لذا قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لنتمكن من التدرب على استخدام الفهارس، وسنشرح في القسم التالي تفاصيل حول الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية، والتي سنستخدمها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل بخادم MySQL وسننشئ قاعدة بيانات تجريبية لاتباع الأمثلة الواردة في هذا المقال. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نتصل بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات باسم indexes: mysql> CREATE DATABASE indexes; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات indexes من خلال تنفيذ تعليمة USE التالية: $ USE indexes; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، وسننشئ جدولًا تجريبيًا ضمنها، حيث سنستخدم في هذا المقال قاعدة بيانات افتراضية للموظفين لتخزين تفاصيل الموظفين الحاليين وأجهزة عملهم. سيحتوي الجدول employees على بيانات بسيطة حول الموظفين في قاعدة البيانات، والتي سنمثّلها باستخدام الأعمدة التالية: employee_id: معرّف الموظف، نوع بياناته int، وسيكون هذا العمود المفتاح الرئيسي Primary Key للجدول first_name: الاسم الأول لكل موظف، نوع بياناته varchar بحد أقصى 50 محرفًا last_name: لاسم الأخير لكل موظف، ونمثّله باستخدام نوع بياناته varchar بحد أقصى 50 محرفًا device_serial: الرقم التسلسلي لحاسوب الموظف، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 15 محرفًا salary: راتب كل موظف، ونمثّله باستخدام نوع البيانات int الذي يخزّن البيانات العددية ننشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE employees ( mysql> employee_id int, mysql> first_name varchar(50), mysql> last_name varchar(50), mysql> device_serial varchar(15), mysql> salary int mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول employees ببعض البيانات التجريبية من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO employees VALUES mysql> (1, 'John', 'Smith', 'ABC123', 60000), mysql> (2, 'Jane', 'Doe', 'DEF456', 65000), mysql> (3, 'Bob', 'Johnson', 'GHI789', 70000), mysql> (4, 'Sally', 'Fields', 'JKL012', 75000), mysql> (5, 'Michael', 'Smith', 'MNO345', 80000), mysql> (6, 'Emily', 'Jones', 'PQR678', 85000), mysql> (7, 'David', 'Williams', 'STU901', 90000), mysql> (8, 'Sarah', 'Johnson', 'VWX234', 95000), mysql> (9, 'James', 'Brown', 'YZA567', 100000), mysql> (10, 'Emma', 'Miller', 'BCD890', 105000), mysql> (11, 'William', 'Davis', 'EFG123', 110000), mysql> (12, 'Olivia', 'Garcia', 'HIJ456', 115000), mysql> (13, 'Christopher', 'Rodriguez', 'KLM789', 120000), mysql> (14, 'Isabella', 'Wilson', 'NOP012', 125000), mysql> (15, 'Matthew', 'Martinez', 'QRS345', 130000), mysql> (16, 'Sophia', 'Anderson', 'TUV678', 135000), mysql> (17, 'Daniel', 'Smith', 'WXY901', 140000), mysql> (18, 'Mia', 'Thomas', 'ZAB234', 145000), mysql> (19, 'Joseph', 'Hernandez', 'CDE567', 150000), mysql> (20, 'Abigail', 'Smith', 'FGH890', 155000); ستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 20 rows affected (0.010 sec) Records: 20 Duplicates: 0 Warnings: 0 ملاحظة: مجموعة البيانات هنا ليست كبيرة بما يكفي لتوضيح تأثير الفهارس على الأداء مباشرة، فالهدف منها هنا توضيح لكيفية استخدام الفهارس في MySQL لتقييد عدد الصفوف التي نجتازها لإجراء الاستعلامات والحصول على النتائج المطلوبة. نحن الآن جاهزون لمتابعة هذا المقال والبدء باستخدام الفهارس في MySQL. ما هي الفهارس Indexes يجب أن تمر قاعدة البيانات على جميع الصفوف الموجودة في الجدول واحدًا تلو الآخر عند تنفيذ استعلام على قاعدة بيانات MySQL، فمثلًا قد نرغب في البحث عن الاسم الأخير للموظفين المتطابق مع الاسم Smith أو جميع الموظفين الذين يتقاضون راتبًا أعلى من 100000 دولار، حيث سيُفحَص كل صف في الجدول واحدًا تلو الآخر للتحقق مما إذا كان يتطابق مع الشرط. إذا كان الصف متطابقًا مع الشرط، فسيُضاف إلى قائمة الصفوف المُعادة، وإن لم يكن كذلك، فسيمسح MySQL الصفوف اللاحقة حتى يستعرض الجدول بالكامل. هذه الطريقة للعثور على الصفوف المطابقة فعّالة، ولكنها قد تصبح بطيئة وتستهلك كثيرًا من الموارد عند زيادة حجم الجدول، لذا قد لا تكون مناسبة للجداول الكبيرة أو الاستعلامات التي تتطلب وصولًا متكررًا أو سريعًا إلى البيانات. يمكن حل مشكلات الأداء المتعلقة بالجداول والاستعلامات الكبيرة باستخدام الفهارس، وهي هياكل بيانات فريدة تخزّن مجموعة فرعية مرتبة من البيانات فقط بحيث تكون منفصلة عن صفوف الجدول، وتسمح لمحرّك قاعدة البيانات بالعمل بسرعة وكفاءة أكبر عند البحث عن القيم أو الترتيب وفق حقل معين أو مجموعة من الحقول. لنستخدم الآن الجدول employees، فأحد الاستعلامات النموذجية التي يمكن تنفيذها هو العثور على الموظفين باستخدام اسمهم الأخير. إن لم نستخدم الفهارس، فسيسترجع MySQL كل موظف من الجدول ويتحقق من تطابق الاسم الأخير مع الاستعلام، وإذا استخدمنا فهرسًا ما، فسيحتفظ بقائمة منفصلة من الأسماء الأخيرة، والتي تحتوي فقط على مؤشّرات إلى صفوف الموظفين المحدَّدين في الجدول الرئيسي، ثم سيستخدم هذا الفهرس لاسترجاع النتائج دون مسح الجدول بأكمله. يمكن تشبيه الفهارس بدليل الهاتف، فإذا أردنا تحديد موقع شخص اسمه John Smith في هذا الدليل، ننتقل أولًا إلى الصفحة الصحيحة التي تسرد الأشخاص الذين تبدأ أسماؤهم بالحرف S، ثم نبحث في الصفحات عن الأشخاص الذين تبدأ أسماؤهم بالحرفين Sm، وبذلك يمكن استبعاد العديد من الإدخالات بسرعة، مع العلم أنها لا تتطابق مع الشخص الذي نبحث عنه. تعمل هذه العملية بنجاح لأن البيانات في دليل الهاتف مرتبة أبجديًا، وهو أمر نادر الحدوث مع البيانات المخزَّنة مباشرةً في قاعدة البيانات. يمثّل الفهرس في محرّك قاعدة البيانات غرضًا مشابهًا لدليل الهاتف، حيث يحتفظ بالمراجع المرتبة أبجديًا إلى البيانات، وبالتالي يساعد قاعدة البيانات في العثور على الصفوف المطلوبة بسرعة. لاستخدام الفهارس فوائد متعددة، وأكثرها شيوعًا هو تسريع استعلامات WHERE الشرطية، وفرز البيانات باستخدام تعليمات ORDER BY بسرعة أكبر، وفرض أن تكون القيم فريدة، لكن من ناحية أخرى قد يؤدي استخدام الفهارس إلى تراجع أداء قاعدة البيانات في بعض الظروف، فهي مصممة الفهارس لتسريع استرجاع البيانات وتُنفَّذ باستخدام هياكل بيانات إضافية مخزَّنة مع بيانات الجدول، ويجب تحديث هذه الهياكل عند كل تغيير في قاعدة البيانات، مما قد يؤدي إلى إبطاء أداء استعلامات INSERT و UPDATE و DELETE. لكن إذا كان لدينا مجموعات بيانات كبيرة تتغير كثيرًا، فستتفوق الفوائد الناتجة عن السرعة المُحسَّنة لاستعلامات SELECT أحيانًا على الأداء الأبطأ الملحوظ للاستعلامات التي تكتب البيانات في قاعدة البيانات. يُفضَّل إنشاء الفهارس عند وجود حاجة واضحة إليها فقط مثل الوقت الذي يبدأ فيه أداء التطبيق في الانخفاض. نحتاج لأن نضع في الاعتبار الاستعلامات التي تُنفَّذ بشكل متكرر وتستغرق وقتًا أطول عند اختيار الفهارس التي سننشئها، ونبني الفهارس بناءً على شروط الاستعلام التي ستستفيد منها أكثر من غيرها. ملاحظة: نركز في هذا المقال على شرح فهارس قاعدة البيانات في MySQL وتوضيح تطبيقاتها الشائعة وأنواعها، حيث يدعم محرّك قاعدة البيانات عدة سيناريوهات أكثر تعقيدًا لاستخدام الفهارس لزيادة أداء قاعدة البيانات، لكن هذا خارج نطاق هذا المقال. ويمكن مطالعة توثيق MySQL الرسمي حول الفهارس للحصول على معلومات وافية عن مميزات فهارس قاعدة البيانات. ستنشئ في الأقسام التالية فهارس من أنواع مختلفة لمجموعة من السيناريوهات، وسنتعلم كيفية التحقق من استخدام الفهارس في الاستعلام، وكيفية إزالة الفهارس عند الحاجة. استخدام فهارس العمود الواحد Single-Column فهرس العمود الواحد هو أحد أكثر أنواع الفهارس شيوعًا ووضوحًا، حيث يمكن استخدامه لتحسين أداء الاستعلام، ويساعد هذا النوع من الفهارس قاعدة البيانات على تسريع الاستعلامات التي ترشّح مجموعة البيانات بناءً على قيم عمود واحد. يمكن للفهارس التي أنشأناها على عمود واحد تسريع العديد من الاستعلامات الشرطية التي تستخدم المطابقات التامة باستخدام المعامل = والمقارنات باستخدام > أو <. لا توجد فهارس في قاعدة البيانات التجريبية التي أنشأناها في خطوة سابقة. سنختبر أولًا كيفية تعامل قاعدة البيانات مع استعلامات SELECT للجدول employees عند استخدام التعليمة WHERE لطلب مجموعة فرعية من البيانات من الجدول فقط قبل إنشاء الفهرس. لنفترض أننا نريد العثور على الموظفين الذين راتبهم يساوي 100000 دولار أمريكي تمامًا من خلال تنفيذ الاستعلام التالي: mysql> SELECT * FROM employees WHERE salary = 100000; تطلب التعليمة WHERE مطابقة تامة للموظفين الذين يتطابق راتبهم مع القيمة المطلوبة، وستستجيب قاعدة البيانات كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 9 | James | Brown | YZA567 | 100000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) ملاحظة: استجابت قاعدة البيانات استجابةً آنية تقريبًا للاستعلام كما يظهر الخرج السابق، فلن يؤثر استخدام الفهارس بوضوح على أداء الاستعلام مع وجود عدد قليل من الصفوف في قاعدة البيانات، ولكن سنلاحظ تغييرات كبيرة في زمن تنفيذ الاستعلام في حال تنفيذه على مجموعات البيانات الكبيرة. لا يمكن معرفة كيفية تعامل محرّك قاعدة البيانات مع مسألة العثور على الصفوف المطابقة في الجدول بالاعتماد على خرج الاستعلام فقط، ولكن يوفّر MySQL طريقة لمعرفة الطريقة التي ينفّذ بها المحرّك الاستعلام باستخدام التعليمة EXPLAIN، حيث يمكننا مثلًا الوصول إلى طريقة تنفيذ الاستعلام SELECT من خلال تنفيذ الأمر التالي: mysql> EXPLAIN SELECT * FROM employees WHERE salary = 100000; يخبر الأمر EXPLAIN نظام MySQL بتشغيل استعلام SELECT، ويعرض معلومات حول كيفية إجراء الاستعلام داخليًا إلى جانب إعادة النتائج، وستكون نتيجة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | NULL | NULL | NULL | NULL | 20 | 10.00 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) توضّح الأعمدة في جدول الخرج السابق العديد من جوانب تنفيذ الاستعلام، وقد يحتوي الخرج على أعمدة إضافية بناءً على إصدار MySQL، وفيما يلي أهم هذه المعلومات: يسرد possible_keys الفهارس التي اعتمدها MySQL للاستخدام، حيث لا يوجد فهارس في حالتنا NULL يمثل key الفهرس الذي قرّر MySQL استخدامه عند تنفيذ الاستعلام، حيث لم نستخدم أي فهرس في مثالنا NULL. يحدد rows عدد الصفوف التي يجب على MySQL تحليلها قبل إعادة النتائج، وتبلغ قيمته 20 في مثالنا وهو يمثّل عدد جميع الصفوف الممكنة في الجدول، مما يعني أنه يجب على MySQL مسح جميع الصفوف في الجدول employees للعثور على الصف الوحيد المُعاد يعرض Extra معلومات إضافية تصف خطة الاستعلام، حيث تعني Using where في مثالنا أن قاعدة البيانات رشّحت النتائج مباشرة من الجدول باستخدام التعليمة WHERE تجدر الإشارة لأنه يجب على قاعدة البيانات مسح 20 صفًا لاسترجاع صف واحد في حالة عدم وجود فهرس، وإذا احتوى الجدول على ملايين الصفوف، فيجب على MySQL المرور عليها واحدًا تلو الآخر، مما يؤدي إلى ضعف أداء الاستعلام. ملاحظة: تعرض إصدارات MySQL الأحدث العبارة 1 row in set, 1 warning في الخرج عند استخدام التعليمة EXPLAIN، بينما تعرض إصدارات MySQL الأقدم وقواعد البيانات المتوافقة مع MySQL العبارة 1 row in set، ولا يُعَد التحذير علامة على وجود مشكلة، حيث يستخدم MySQL آلية التحذيرات الخاصة به لتوفير مزيد من المعلومات الموسَّعة حول خطة الاستعلام. يُعَد هذا الاستخدام لهذه المعلومات الإضافية خارج نطاق هذا المقال، حيث يمكنك معرفة المزيد حول هذا السلوك في صفحة تنسيق خرج التعليمة EXPLAIN المُوسَّع في توثيق MySQL. استخدم استعلام SELECT الذي نفّذته سابقًا شرط المساواة WHERE salary = 100000، ولكن لنتحقق مما إذا كانت قاعدة البيانات ستتصرف بطريقة مماثلة مع شرط المقارنة، ونجرب استرجاع الموظفين الذين راتبهم أقل من 70000: mysql> SELECT * FROM employees WHERE salary < 70000; أعادت قاعدة البيانات هذه المرة صفين John Smith و Jane Doe كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | | 2 | Jane | Doe | DEF456 | 65000 | +-------------+------------+-----------+---------------+--------+ 8 rows in set (0.000 sec) ولكن إذا استخدمنا التعليمة EXPLAIN لفهم تنفيذ الاستعلام كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE salary < 70000; فسنلاحظ أن الجدول مطابق تقريبًا للاستعلام السابق: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | NULL | NULL | NULL | NULL | 20 | 33.33 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) مسح MySQL جميع الصفوف 20 في الجدول للعثور على الصفوف التي طلبتها باستخدام تعليمة WHERE في الاستعلام كما هو الحال مع الاستعلام السابق. يُعَد عدد الصفوف المُعادة في الخرج السابق صغيرًا مقارنة بعدد جميع الصفوف في الجدول، ولكن يجب على محرّك قاعدة البيانات إنجاز الكثير من العمل للعثور عليها. يمكن حل هذه المشكلة من خلال إنشاء فهرس للعمود salary، والذي سيخبر MySQL بالحفاظ على هيكل بيانات إضافي ومُحسَّن، وخاصةً لبيانات العمود salary من الجدول employees، لذا ننفّذ الاستعلام التالي: mysql> CREATE INDEX salary ON employees(salary); تتطلب صيغة التعليمة CREATE INDEX ما يلي: اسم الفهرس وهو salary في مثالنا، ويجب أن يكون اسم الفهرس فريدًا في الجدول الواحد ويمكن تكراره بجداول مختلفة في قاعدة البيانات اسم الجدول الذي أنشأنا الفهرس له، وهو employees في مثالنا قائمة الأعمدة التي أنشأنا الفهرس لها، حيث استخدمنا في مثالنا عمودًا واحدًا بالاسم salary لبناء الفهرس قد يظهر الخطأ التالي: ERROR 1142 (42000): INDEX command denied to user 'user'@'host' for table 'employees' عند تنفيذ الأمر CREATE INDEX بناءً على أذونات مستخدم MySQL، حيث يمكن منح أذونات INDEX للمستخدم من خلال تسجيل الدخول إلى MySQL كمستخدم جذر وتنفيذ الأوامر التالية مع تعديل اسم مستخدم MySQL والمضيف حسب الحاجة: mysql> GRANT INDEX on *.* TO 'user'@'localhost'; mysql> FLUSH PRIVILEGES; نسجّل الخروج كمستخدم جذر ونسجّل الدخول مرة أخرى كمستخدم عادي بعد تحديث أذونات المستخدم، ثم نعيد تشغيل التعليمة CREATE INDEX، ستؤكّد الآن قاعدة البيانات إنشاء الفهرس بنجاح كما يلي: الخرج Query OK, 0 rows affected (0.024 sec) Records: 0 Duplicates: 0 Warnings: 0 نجرّب تكرار الاستعلامات السابقة للتحقق مما إذا كان هناك أي تغيير عند استخدام الفهرس، لذا نبدأ باسترجاع الموظف الذي يتقاضى راتبًا قدره 100000 بالضبط كما يلي: mysql> SELECT * FROM employees WHERE salary = 100000; وستبقى النتيجة نفسها مع إعادة الموظف James Brown فقط كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 9 | James | Brown | YZA567 | 100000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) وإذا طلبنا من MySQL شرحَ كيفية تعامله مع الاستعلام، فسيعرض بعض الاختلافات عمّا سبق، لذا ننفّذ تعليمة EXPLAIN كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE salary = 100000; وسيكون الخرج هذه المرة كما يلي: الخرج +----+-------------+-----------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | ref | salary | salary | 5 | const | 1 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+--------+---------+-------+------+----------+-------+ 1 row in set, 1 warning (0.00 sec) يصرّح MySQL أنه قرّر استخدام المفتاح الذي اسمه salary من المفتاح الوحيد الموضح في العمود possible_keys، وهذا المفتاح هو الفهرس الذي أنشأناه. يعرض العمود rows الآن القيمة 1 بدلًا من 20، حيث تجنّبت قاعدة البيانات مسح جميع الصفوف في قاعدة البيانات ويمكنها إعادة الصف المطلوب مباشرة لأنها استخدمت الفهرس. لا يذكر العمود Extra الآن العبارة Using WHERE، لأن التكرار على الجدول الرئيسي والتحقق من أن كل صف يحقق شرط الاستعلام لم يكن ضروريًا لإجراء الاستعلام. وكما ذكرنا سابقًا لن نلاحظ تأثير استخدام الفهرس جدًا مع مجموعة بيانات تجريبية صغيرة، ولكن تطلّب الأمر من قاعدة البيانات عملًا أقل بكثير لاسترجاع النتيجة وسيكون تأثير هذا التغيير كبيرًا على مجموعة بيانات أكبر. نجرّب إعادة تشغيل الاستعلام الثاني واسترجاع الموظفين الذين راتبهم أقل من 70000 للتحقق من استخدام الفهرس، لذا نفّذ الاستعلام التالي: mysql> SELECT * FROM employees WHERE salary < 70000; نلاحظ إعادة بيانات John Smith و Jane Doe أيضًا كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | | 2 | Jane | Doe | DEF456 | 65000 | +-------------+------------+-----------+---------------+--------+ 8 rows in set (0.000 sec) ولكن إذا استخدمنا تعليمة EXPLAIN كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE salary < 70000; فسيكون الجدول مختلفًا عن التنفيذ السابق للاستعلام نفسه كما يلي: الخرج +----+-------------+-----------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | employees | NULL | range | salary | salary | 5 | NULL | 2 | 100.00 | Using index condition | +----+-------------+-----------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+ 1 row in set, 1 warning (0.00 sec) يخبرنا العمود key أن MySQL استخدم الفهرس لإجراء الاستعلام، ويخبرنا العمود rows بتحليل صفين فقط لإعادة النتيجة. يحتوي العمود Extra الآن على العبارة Using index condition، مما يعني أن MySQL أجرى ترشيحًا باستخدام الفهرس في هذه الحالة ثم استخدم الجدول الأساسي فقط لاسترجاع الصفوف المطابقة فعليًا. ملاحظة: قد يقرر MySQL عدم استخدام الفهرس في بعض الأحيان حتى في حالة وجود الفهرس وإمكانية استخدامه، فمثلًا إذا نفذنا الأمر التالي: mysql> EXPLAIN SELECT * FROM employees WHERE salary < 140000; فستكون خطة التنفيذ كما يلي: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | salary | NULL | NULL | NULL | 20 | 80.00 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) يعني وجود عمود key الفارغ الذي له القيمة NULL أن MySQL قرر عدم استخدام الفهرس، والذي يمكن تأكيده من خلال الصفوف العشرين الممسوحة بالرغم من إدراج الفهرس salary في العمود possible_keys. يحلل مخطِّط استعلام قاعدة البيانات كل استعلام مقابل للفهارس المحتملة لتحديد أسرع مسار للتنفيذ، فإذا كانت تكلفة الوصول إلى الفهرس أكبر من فائدة استخدامه مثل إعادة الاستعلام جزءًا كبيرًا من بيانات الجدول الأصلية، فيمكن لقاعدة البيانات أن تقرر أنه من الأسرع إجراء مسح كامل للجدول فعليًا. توضّح التعليقات في العمود Extra مثل Using index condition أو Using where كيفية تنفيذ محرّك قاعدة البيانات للاستعلام بمزيد من التفصيل، فقد تختار قاعدة البيانات طريقة أخرى لتنفيذ الاستعلام وقد يكون لدينا خرج مع عدم وجود التعليق Using index condition أو أي تعليق آخر اعتمادًا على السياق. لا يعني ذلك عدم استخدام الفهرس استخدامًا صحيحًا، ولكنه يعني أن قاعدة البيانات قرّرت أن الطريقة الأخرى للوصول إلى الصفوف ستكون أفضل في الأداء. أنشأنا واستخدمنا في هذا القسم فهارس مؤلفة من عمود واحد لتحسين أداء استعلامات SELECT التي تعتمد على الترشيح لعمود واحد، وسنتعرّف في القسم التالي على كيفية استخدام الفهارس لضمان أن تكون القيم فريدة في عمود معين. استخدام الفهارس الفريدة لمنع تكرار البيانات أحد الاستخدامات الشائعة للفهارس هو استرجاع البيانات بسرعة من خلال مساعدة محرّك قاعدة البيانات على إجراء عمل أقل لتحقيق النتيجة نفسها كما وضّحنا سابقًا، وهناك استخدام آخر وهو ضمان عدم تكرار البيانات في جزء الجدول الذي عرّفنا الفهرس له، وهذا ما يفعله الفهرس الفريد Unique Index. إن تجنب القيم المكررة ضروري لضمان سلامة البيانات سواءً من وجهة نظر منطقية أو تقنية، فمثلًا لا ينبغي أن يكون هناك شخصان يستخدمان نفس رقم الضمان الاجتماعي، ولا ينبغي لنظام عبر الإنترنت أن يسمح لمستخدمين متعددين أن يسجّلوا باستخدام اسم المستخدم أو عنوان البريد الإلكتروني نفسه. في حالة جدولنا employees لا ينبغي أن يحتوي حقل الرقم التسلسلي على قيمٍ مكررة وإذا كان الأمر كذلك، فهذا قد يتسبب في منح أكثر من موظف الحاسوب نفسه، ففي هذا الجدول يمكن بسهولة إدخال موظفين جدد مع أرقام تسلسلية مكررة. لنحاول إدخال موظف آخر مع رقم تسلسلي لجهاز قيد الاستخدام كما يلي: mysql> INSERT INTO employees VALUES (21, 'Sammy', 'Smith', 'ABC123', 65000); ستدرج قاعدة البيانات هذا الصف وتعلمنا بنجاح العملية كما يلي: الخرج Query OK, 1 row affected (0.009 sec) فإذا استعلمنا عن الموظفين باستخدام الحاسوب ذي الرقم التسلسلي ABCD123 كما يلي: mysql> SELECT * FROM employees WHERE device_serial = 'ABC123'; فسنحصل على شخصين مختلفين كما توضح النتيجة التالية: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | | 21 | Sammy | Smith | ABC123 | 65000 | +-------------+------------+-----------+---------------+--------+ 2 rows in set (0.000 sec) هذا ليس سلوكًا متوقعًا لإبقاء قاعدة بيانات employees صالحة. لذا سنتراجع عن هذا التغيير من خلال حذف الصف الأخير الذي أنشأناه كما يلي: mysql> DELETE FROM employees WHERE employee_id = 21; يمكنك التأكد من ذلك من خلال إعادة تشغيل استعلام SELECT السابق كما يلي: mysql> SELECT * FROM employees WHERE device_serial = 'ABC123'; وبالتالي أصبح الموظف John Smith المستخدم الوحيد للجهاز الذي رقمه التسلسلي ABC123 مرة أخرى: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) لننشئ الآن فهرسًا فريدًا للعمود device_serial لحماية قاعدة البيانات من مثل هذه الأخطاء من خلال تنفيذ التعليمة التالية: mysql> CREATE UNIQUE INDEX device_serial ON employees(device_serial); توجِّه إضافة الكلمة المفتاحية UNIQUE عند إنشاء الفهرس قاعدةَ البيانات للتأكد من عدم تكرار القيم في العمود device_serial، حيث تؤدي الفهارس الفريدة إلى التحقق من جميع الصفوف الجديدة المضافة إلى الجدول مقابل الفهرس لتحديد ما إذا كانت قيمة العمود تتوافق مع القيد أم لا. وستؤكد قاعدة البيانات إنشاء الفهرس كما يلي: الخرج Query OK, 0 rows affected (0.021 sec) Records: 0 Duplicates: 0 Warnings: 0 نتحقق الآن من إمكانية إضافة إدخال مكرر إلى الجدول من خلال تشغيل استعلام INSERT من جديد: mysql> INSERT INTO employees VALUES (21, 'Sammy', 'Smith', 'ABC123', 65000); ستظهر رسالة الخطأ التالية هذه المرة: الخرج ERROR 1062 (23000): Duplicate entry 'ABC123' for key 'device_serial' يمكن التحقق من عدم إضافة الصف الجديد إلى الجدول باستخدام استعلام SELECT مرة أخرى: mysql> SELECT * FROM employees WHERE device_serial = 'ABC123'; وسيُعاد صف واحد فقط هذه المرة: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) تعمل الفهارس الفريدة على الحماية من الإدخالات المكررة، وهي أيضًا فهارس وظيفية بالكامل لتسريع الاستعلامات. ويستخدم محرّك قاعدة البيانات الفهارس الفريدة باستخدام الطريقة نفسها في الخطوة السابقة، حيث يمكننا التحقق من ذلك من خلال تنفيذ التعليمة التالية: mysql> EXPLAIN SELECT * FROM employees WHERE device_serial = 'ABC123'; وستكون نتيجة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | const | device_serial | device_serial | 63 | const | 1 | 100.00 | NULL | +----+-------------+-----------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+ 1 row in set, 1 warning (0.00 sec) يظهَر الفهرس device_serial في العمودين possible_keys و key، مما يؤكّد استخدام الفهرس عند تنفيذ الاستعلام. بهذا تعلمنا استخدام الفهارس الفريدة Unique Index للحماية من البيانات المكررة في قاعدة البيانات، وسنستخدم في القسم التالي الفهارس التي تمتد إلى أكثر من عمود واحد. الخلاصة تعلّمنا في هذا المقال ما هي الفهارس واستعرضنا أمثلة متعددة على فهارس العمود الواحد المستخدمة لتسريع استرجاع البيانات من خلال استعلامات SELECT الشرطية، أو للحفاظ على جعل بيانات العمود فريدة، وسنشرح في المقال التالي المزيد حول الفهارس ونوضح كيفية تعريف فهارس متعددة الأعمدة Indexes on Multiple Columns وحالات استخدامها، كما ننصح بالاطلاع على سلسلة تعلم SQL للمزيد حول التعامل مع لغة SQL. ترجمة -وبتصرف- للمقال How To Use Indexes in MySQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا الفهارس Indexes في SQL نظرة سريعة على لغة الاستعلامات الهيكلية SQL فهم قواعد البيانات العلاقية تحسين أداء قواعد بيانات SQL للمطورين
-
سنشرح في هذا المقال من سلسلة دليل جودو مفهوم Delta في مجال صناعة الألعاب، ونوضح كيفية استخدامه. يُعَد معامل دلتا delta أو "زمن دلتا" مفهومًا يُساء فهمه كثيرًا في تطوير الألعاب، لذا سنشرح في هذا المقال كيفية استخدامه وأهمية الحركة المستقلة عن معدل الإطارات وأمثلة عملية لاستخدامه في محرّك الألعاب جودو Godot. ليكن لدينا عقدة Sprite تتحرك عبر الشاشة. إذا كان عرض الشاشة 600 بكسل ونريد أن نعبر الشخصية الرسومية Sprite الشاشة خلال 5 ثوانٍ، فيمكننا استخدام العملية الحسابية التالية لإيجاد السرعة اللازمة لذلك: 600 pixels / 5 seconds = 120 pixels/second سنحرّك الشخصية الرسومية في كل إطار باستخدام الدالة _process()، بحيث إذا شُغِّلت اللعبة بمعدل 60 إطارًا في الثانية، فيمكننا إيجاد الحركة لكل إطار باستخدام العملية الحسابية التالية: 120 pixels/second * 1/60 second/frame = 2 pixels/frame ملاحظة: كما نلاحظ، أن وِحدات المقادير متناسقة في جميع العمليات الحسابية السابقة، لذا لا بد من الانتباه دائمًا إليها لتجنب الوقوع في الأخطاء. تكون الشيفرة البرمجية الضرورية كما يلي: extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(2, 0) func _process(delta): $Sprite.position += movement نشغّل الشيفرة البرمجية السابقة وسنجد أن الصورة تعبر الشاشة خلال 5 ثوانٍ. تحصل المشكلة إذا كان هناك شيء آخر يشغَل وقت الحاسوب، والذي يسمى بالتأخير Lag، الذي يكون له عدة أسباب، حيث يمكن أن يكون السبب هو الشيفرة البرمجية التي نستخدمها أو حتى التطبيقات الأخرى التي تعمل على الحاسوب. وإذا حدث التأخير، فقد يؤدي ذلك إلى زيادة طول الإطار. إذا تخيلنا مثلًا أن معدل الإطارات انخفض إلى النصف بحيث يستغرق كل إطار 1/30 بدلًا من 1/60 من الثانية. فمعنى هذا أن الأمر سيستغرق ضعف الوقت حتى تصل الشخصية الرسومية إلى طرف الشاشة عند التحرك بمعدل 2 بكسل لكل إطار. ستؤدي حتى التقلبات الصغيرة في معدل الإطارات إلى سرعة حركة غير متناسقة، وإذا كانت هذه الشخصية الرسومية رصاصة أو جسمًا سريع الحركة، فلن نرغب في إبطائه بهذه الطريقة؛ إذ يجب أن تكون الحركة مستقلة عن معدل الإطارات. إصلاح مشكلة معدل الإطارات تتضمن الدالة _process() تلقائيًا عند استخدامها معاملًا بالاسم delta يمرّره المحرّك كما في الدالة _physics_process() التي تُستخدَم في الشيفرة البرمجية المتعلقة بالفيزياء. المعامل delta هو قيمة عشرية تمثل الوقت المستغرق منذ الإطار السابق، والذي سيكون 1/60 أو 0.0167 ثانية تقريبًا. يمكننا التوقف عن القلق بشأن مقدار تحريك كل إطار من خلال استخدام المعامل delta، إذ سنحتاج للاهتمام فقط بالسرعة المطلوبة بالبكسلات في الثانية، والتي هي 120 من العمليات الحسابية السابقة. سيعطينا ضرب قيمة delta الخاصة بالمحرك بهذا العدد عددَ البكسلات التي يجب تحريكها في كل إطار، وسيُعدَّل هذا العدد تلقائيًا عند تقلّب زمن الإطار. # 60 إطار في الثانية 120 pixels/second * 1/60 second/frame = 2 pixels/frame # 30 إطار في الثانية 120 pixels/second * 1/30 second/frame = 4 pixels/frame وكما نلاحظ، يبدو أنه إذا انخفض معدل الإطارات إلى النصف (أي تضاعف زمن الإطار)، فيجب أن تتضاعف أيضًا الحركة لكل إطار للحفاظ على السرعة المطلوبة، ولهذا سنعدّل الشيفرة البرمجية كما يلي: extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(120, 0) func _process(delta): $Sprite.position += movement * delta يكون زمن الانتقال متناسقًا عند التشغيل بمعدل 30 إطارًا في الثانية كما يلي: إذا أصبح معدل الإطارات منخفضًا جدًا، فلن تكون الحركة سلسةً بعد الآن، ولكن يبقى الزمن كما هو. استخدام معامل دلتا مع معادِلات الحركة إذا كانت الحركة التي نريد العمل عليها أكثر تعقيدًا، فسيبقى المفهوم كما هو مع إبقاء الوحدة بالثواني وليس بالإطارات، والضرب بمعامل delta لكل إطار. ملاحظة: يُعَد التعامل بالبكسلات والثواني أسهل بكثير لأنه يتعلق بكيفية قياس هذه الكميات في العالم الحقيقي، فمثلًا الجاذبية Gravity هي 100 بكسل/ثانية/ثانية، لذا ستتحرك الكرة بسرعة 200 بكسل/ثانية بعد سقوطها لمدة ثانيتين، وإذا استخدمنا وحدة الإطارات، فيجب استخدام التسارع Acceleration بوحدة البكسل/إطار/إطار، ولكننا سنجد أن هذه الوحدة غير مألوفة. إذا طبّقنا الجاذبية مثلًا، فإنها تمثّل التسارع، بحيث ستزيد السرعة بمقدارٍ معين في كل إطار، وستغير السرعة موضع العقدة كما هو الحال في المثال السابق؛ وهنا سنضبط قيم delta و target_fps في الشيفرة البرمجية التالية لمعرفة النتائج: extends Node2D # التسارع بالبكسل/ثانية/ثانية var gravity = Vector2(0, 120) # التسارع بالبكسل/إطار/إطار var gravity_frame = Vector2(0, .033) # السرعة بالبكسل/ثانية أو بالبكسل/إطار var velocity = Vector2.ZERO var use_delta = false var target_fps = 60 func _ready(): Engine.target_fps = target_fps func _process(delta): if use_delta: velocity += gravity * delta $Sprite.position += velocity * delta else: velocity += gravity_frame $Sprite.position += velocity وكما هو ظاهر، فقد ضربنا القيمة المحدثة في الخطوة الزمنية لكل إطار لتحديث السرعة velocity والموضع position، إذ يجب ضرب أي كمية مُحدَّثة في كل إطار بقيمة delta لضمان تغيرها بحيث تكون مستقلة عن معدل الإطارات. استخدام الدوال الحركية Kinematic استخدمنا Sprite للتبسيط في الأمثلة السابقة، مع تحديث الموضع position في كل إطار. إذا استخدمنا جسمًا حركيًا Kinematic ثنائي الأبعاد أو ثلاثي الأبعاد، فسنحتاج لاستخدام أحد توابع الحركة الخاصة به بدلًا من ذلك، خاصةً في حالة استخدام التابع move_and_slide()؛ إذ قد يحدث بعض الارتباك لأنه يستخدم متجه السرعة وليس الموضع، وهذا يعني أننا لن نضرب السرعة بقيمة delta لإيجاد المسافة، إذ تنجز الدالة ذلك نيابةً عنا، ولكن يجب تطبيقها على أيّ عمليات حسابية أخرى مثل التسارع كما في المثال التالي: # الشيفرة البرمجية لحركة الشخصية الرسومية: velocity += gravity * delta position += velocity * delta # الشيفرة البرمجية لحركة الجسم الحركي: velocity += gravity * delta move_and_slide() إن لم نستخدم قيمة delta عند تطبيق التسارع على السرعة، فسيكون التسارع عرضةً للتقلبات في معدل الإطارات، وقد يكون لذلك تأثير أكثر دقةً على الحركة؛ إذ سيكون غير متناسق مع وجود صعوبة في ملاحظته. ملاحظة: يجب أيضًا تطبيق قيمة delta على أي كميات أخرى مثل الجاذبية والاحتكاك وغير ذلك عند استخدام التابع move_and_slide(). ختامًا بهذا نكون قد تعرفنا على مفهوم delta في مجال تطوير الألعاب وكيفية استخدامه، وسنتعرف في المقال التالي على كيفية حفظ واسترجاع البيانات المحلية بين جلسات اللعب. ترجمة -وبتصرّف- للقسم Understanding delta من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: الطريقة الصحيحة للتواصل بين العقد في جودو Godot دليلك الشامل إلى برمجة الألعاب مطور الألعاب: من هو وما هي مهامه
-
سننتعرف في مقال اليوم على صيغة ملفات GGUF التي تستخدم لتخزين النماذج للاستدلال Inference باستخدام مكتبة GGML والمكتبات الأخرى التي تعتمد عليها مثل llama.cpp أو whisper.cpp الشهيرتين. يدعم مستودع Hub هذه الصيغة مع ميزات تسمح بالفحص السريع للموترات Tensors والبيانات الوصفية داخل الملف. صُمِّمت هذه الصيغة بوصفها "صيغة ملف واحد"، حيث يحتوي الملف الواحد عادةً على كلٍّ من سمات الضبط Configuration Attributes ومفردات المرمِّز Tokenizer Vocabulary وسمات أخرى، بالإضافة إلى جميع الموترات المُراد تحميلها في النموذج. تأتي هذه الملفات بصيغ مختلفة وفقًا لنوع تكميم Quantization الملف، لذا اطّلع على بعض منها. استخدامات صيغة GGUF يفيد استخدام صيغة GGUF في العديد من الجوانب تشمل: تخزين النماذج بكفاءة حيث تسهل ملفات GGUF تخزين جميع المعلومات المتعلقة بالنموذج في ملف واحد، مما يبسط عملية تحميل وتشغيل النماذج في بيئات مختلفة تدعم صيغة GGUF أنواعًا مختلفة من التكميم مثل F32، Q4_K، و Q6_K، مما يساعد على تقليل حجم النماذج مع الحفاظ على أدائها تتكامل مع مكتبات التحويل حيث يمكن استخدام ملفات GGUF مع مكتبة المحولات transformers لتحميل النماذج والمرمزين مباشرةً، مما يسهل عملية الاستدلال أو التدريب المخصص دعم صيغة GGUF ضمن مكتبة المحولات Transformers لقد أُضفيت القدرة على تحميل ملفات gguf ضمن مكتبة المحوِّلات transformers لتقديم مزيد من إمكانات التدريب أو الصقل Fine-tuning لنماذج gguf قبل تحويل هذه النماذج مرةً أخرى إلى صيغة gguf لاستخدامها ضمن نظام ggml البيئي، ويجب إلغاء تكميم النموذج إلى صيغة fp32 عند تحميله قبل تحميل الأوزان المُراد استخدامها في إطار عمل PyTorch. ملاحظة: لا يزال الدعم في مراحله الأولى والمساهمات مرحَّبٌ بها لترسيخ هذا الدعم على أنواع التكميم وبنى النماذج المختلفة. سنوضّح فيما يلي بنى النماذج وأنواع التكميم المدعومة حاليًا. أنواع التكميم المدعومة تُحدَّد أنواع التكميم المدعومة الأولية وفقًا لملفات التكميم الشائعة التي جرت مشاركتها على مستودع Hub وهي: F32 Q2_K Q3_K Q4_0 Q4_K Q5_K Q6_K Q8_0 اطّلع على مثال من محلّل بايثون Python الممتاز 99991/pygguf لإلغاء تكميم Dequantize الأوزان. ملاحظة: التكميم هو عملية ضغط تقلل من حجم النموذج عن طريق تقريب القيم داخل النموذج إلى نطاقات أقل دقة، مما يقلل من متطلبات التخزين والذاكرة. بنى النماذج المدعومة بنى النماذج المدعومة حاليًا هي البنى الشائعة جدًا على مستودع Hub، وهي: LLaMa Mistral Qwen2 مثال لاستخدام صيغة ملفات GGUF يمكننا تحميل ملفات gguf في مكتبة transformers من خلال تحديد الوسيط gguf_file لتوابع from_pretrained لكلٍّ من المرمِّزات والنماذج. وفيما يلي كيفية تحميل المرمِّز والنموذج، حيث يمكن تحميلهما من الملف نفسه: from transformers import AutoTokenizer, AutoModelForCausalLM model_id = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF" filename = "tinyllama-1.1b-chat-v1.0.Q6_K.gguf" tokenizer = AutoTokenizer.from_pretrained(model_id, gguf_file=filename) model = AutoModelForCausalLM.from_pretrained(model_id, gguf_file=filename) وبهذا يمكننا الآن الوصول إلى الإصدار الكامل غير المكمَّم من النموذج في نظام PyTorch البيئي، والذي يمكن دمجه مع مجموعة كبيرة من الأدوات الأخرى. يُوصَى باستخدام ملف convert-hf-to-gguf.py من نموذج llama.cpp للتحويل مرةً أخرى إلى ملف gguf. نوضّح فيما يلي كيفية إكمال السكربت السابق لحفظ النموذج وتصديره مرةً أخرى إلى صيغة gguf: tokenizer.save_pretrained('directory') model.save_pretrained('directory') !python ${path_to_llama_cpp}/convert-hf-to-gguf.py ${directory} ترجمة -وبتصرّف- للقسم GGUF and interaction with Transformers من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: استكشاف الأخطاء وإصلاحها في مكتبة المحولات Transformers تعرف على مكتبة المحوّلات Transformers من منصة Hugging Face تثبيت مكتبة المحوّلات Transformers
-
بعد أن تعرفنا في المقال السابق من سلسلة دليل جودو على كيفية ترتيب معالجة العقد والتنقل في شجرة المشاهد في محرك الألعاب جودو Godot، سنتعرف في هذا المقال على الطريقة السليمة للتواصل بين العقد Nodes ونوضح المشاكل التي قد تحدث عند القيام بممارسات غير متوافقة مع الطريقة الصحيحة المنصوح بها. كما هو معروف، إذا أصبحت لدينا مشاهد ونسخ متعددة مع عدد كبير من العقد، فسيصبح مشروعنا معقدًا؛ وعندها قد نكتب شيفرة برمجية تشبه ما يلي: get_node("../../SomeNode/SomeOtherNode") get_parent().get_parent().get_node("SomeNode") get_tree().get_root().get_node("SomeNode/SomeOtherNode") وهنا إذا كتبنا الكود بهذا الشكل، فسنجد أن مثل مراجع References هذه العقد ستنكسر بكل سهولة، بحيث قد يتسبب إحداث تغيير بسيط في شجرة المشاهد في جعل مراجع العقد غير صالحة؛ ولهذا السبب، لا يجب أن يكون الاتصال بين العقد والمشاهد معقدًا. يجب أن تدير العقد أبناءها وليس العكس؛ حيث إذا استخدمنا الدالة get_parent() أو get_node("..")، فيُحتمَل أننا نتجه إلى مشكلة ما، إذ تكون مثل هذه المسارات للعقد سهلة الكسر. سنذكر ففيما يلي المشاكل الرئيسية الثلاث في هذا الترتيب: لا يمكن اختبار مشهد بطريقة مستقلة؛ فإذا شغّلنا المشهد بمفرده أو في مشهد اختبار لا يحتوي على إعداد العقدة نفسه، فستسبّب الدالة get_node() عطلًا لا يمكننا تغيير الأشياء بسهولة، لأننا إذا قرّرنا إعادة ترتيب أو تصميم الشجرة، فلن تكون المسارات صالحةً مجددًا يبدأ ترتيب الجاهزية من الأبناء أولًا حتى الوصول إلى الأب أخيرًا، وهذا يعني فشل محاولة الوصول إلى خاصية الأب في التابع _ready() الخاصة بالعقدة، لأن الأب غير جاهزٍ بعد ملاحظة: يُنصح بالاطلاع على المقال السابق للحصول على شرح لكيفية دخول العقد إلى الشجرة وكيف تصبح جاهزة. لا بد من توفر إمكانية إنشاء نسخة لعقدة أو مشهد في أيّ مكان في اللعبة التي ننشؤها، مع عدم افتراض أيّ شيء حول ما سيكون عليه الأب. سنستخدم أمثلة مفصلة لاحقًا، ولكن سنتحدث الآن عن القاعدة الذهبية لتواصل العقد، والتي هي: "استدعاء للأسفل، وإشارة Signal للأعلى". تشير هذه القاعدة إلى أنه في حال استدعَت العقدة ابنًا، بمعنى تنقلها إلى أسفل الشجرة، فسيكون استدعاء الدالة get_node() مناسبًا؛ وإذا كانت العقدة بحاجة إلى التواصل مع أعلى الشجرة، فيجب استخدام إشارة لذلك. يؤدي استخدام هذه القاعدة عند تصميم إعداد المشهد الخاص بنا إلى الحصول على مشروع قابل للصيانة ومنظم جيدًا، وسنتجنب بذلك استخدام مسارات العقد المعقدة التي تؤدي إلى مشاكل. لنلقِ الآن نظرةً على كلٍّ من هذه الاستراتيجيات مع بعض الأمثلة. استخدام الدالة get_node() يعبُر استدعاء الدالة get_node() شجرة المشاهد باستخدام مسار معين للعثور على العقدة المسماة. مثال عن استخدام get_node() ليكن لدينا الضبط التالي: يجب أن يُعلِم السكربت الموجود في العقدة Player العقدةَ AnimatedSprite2D بالرسوم المتحركة التي تُشغَّل بناءً على حركة اللاعب، بحيث تعمل الدالة get_node() في هذه الحالة بنجاح كما يلي: extends CharacterBody2D func _process(delta): if speed > 0: get_node("AnimatedSprite2D").play("run") else: get_node("AnimatedSprite2D").play("idle") ملاحظة: يمكننا استخدام المحرف $ في لغة GDScript كاختصار لاستدعاء get_node()، لذا بإمكاننا كتابة $AnimatedSprite2D مباشرةً. طريقة أفضل للاستدعاء تتمثل سلبية الطريقة السابقة في تحديد مسار العقدة، حيث إذا تغير هذا المسار لاحقًا، فيجب تعديل الشيفرة البرمجية أيضًا؛ ولهذا يمكننا استخدام الميزة @export لتحديد عقدة مباشرةً كما يلي: extends CharacterBody2D @export var animation : AnimatedSprite2D func _process(delta): if speed > 0: animation.play("run") else: animation.play("idle") يمكننا باستخدام هذه الطريقة إسناد قيمةٍ إلى المتغير مباشرةً في الفاحص Inspector من خلال اختيار العقدة. استخدام الإشارات Signals يجب استخدام الإشارات لاستدعاء الدوال في العقد الموجودة في مستوًى أعلى من الشجرة أو الموجودة على المستوى نفسه (أي العقد الأشقاء Siblings). يمكننا توصيل إشارة في المحرّر للعقد الموجودة قبل بدء اللعبة في أغلب الأحيان أو في الشيفرة البرمجية للعقد التي تنشِئ نسخةً منها في وقت التشغيل، وتكون صيغة توصيل الإشارة كما يلي: signal_name.connect(target_node.target_function) عند تجربة الاتصال بعقدة شقيقة، قد يبدو أننا سنحتاج إلى مسارات للعقدة، مثل المسار ../Sibling، ولكن كما نرى، ذلك يخالف القاعدة السابقة. ولتفادي أي خلط، لا بد من التأكد دائمًا من أن الأب المشترك هو الذي يجري الاتصالات، نظرًا لتمكن عقدة الأب التي تُعَد أبًا مشتركًا لعقدتي الإشارة والاستقبال من تحديد مكانهما وأنها ستكون جاهزة بعدهما باتباع قاعدة الاستدعاء في الشجرة من الأعلى للأسفل. مثال عن استخدام الإشارات يُعَد تحديث واجهة المستخدم UI الخاصة بك حالة استخدام شائعة جدًا للإشارات، لأننا نريد مثلًا تحديث عرض Label أو ProgressBar كلما اختلف المتغير health الخاص باللاعب، ولكن تكون عقد واجهة المستخدم الخاصة بك منفصلة تمامًا عن اللاعب إذ لا يعرف اللاعب شيئًا عن مكان هذه العقد وكيفية العثور عليها. ليكن لدينا إعداد المثال التالي: يمكننا ملاحظة أن واجهة المستخدم هي نسخة من مشهد، لأننا نعرض العقد المضمَّنة فقط، وهو المكان الذي نرى فيه أشياء مثل get_node("../UI/VBoxContainer/HBoxContainer/Label).text = str(health) التي نريد تجنبها، لذا يصدر اللاعب بدلًا من ذلك إشارة health_changed كلما أضاف أو فقد جزءًا من صحته، ويجب إرسال هذه الإشارة إلى الدالة update_health() الخاصة بواجهة المستخدم UI، والتي تتولّى ضبط قيمة Label. سنستخدم الشيفرة البرمجية التالية في سكربت Player كلما تغيرت صحة اللاعب: health_changed.emit(health) لدينا ما يلي في سكربت واجهة المستخدم UI: onready var label = $VBoxContainer/HBoxContainer/Label func update_health(value): label.text = str(value) كل ما نحتاجه الآن هو توصيل الإشارة بالدالة، والمكان المثالي ذلك موجود في سكربت World الذي يمثّل الأب المشترك للعقدتين ويعرف مكانهما: func _ready(): $Player.health_changed.connect($UI.update_health) استخدام المجموعات تُعَد المجموعات طريقةً أخرى لفك الارتباط بين عقدتين، وخاصةً عندما تكون لدينا الكثير من الكائنات المتشابهة التي تطبّق الشيء نفسه؛ إذ يمكن إضافة عقدة إلى أيّ عددٍ من المجموعات، كما يمكن تغيير العضوية ديناميكيًا في أيّ وقت باستخدام الدالتين add_to_group() و remove_from_group(). من المفاهيم الخاطئة الشائعة حول المجموعات أنها نوع من الكائنات أو المصفوفات التي تحتوي على مراجعٍ للعقد، ولكن المجموعات هي نظام وسم Tagging System، في حين تكون العقدة ضمن مجموعة عند إسناد وسمٍ من هذه المجموعة إليها. تتعقّب شجرة المشهد SceneTree الوسوم ويكون لديها دوال مثل الدالة get_nodes_in_group() للمساعدة في العثور على جميع العقد التي يكون لها وسم معين. مثال عن استخدام المجموعات لتكن لدينا لعبة إطلاق نار فضائية مثل لعبة غالاغا Galaga، مع وجود الكثير من الأعداء الذين يطيرون حول الشخصية الرئيسية، وقد يكون للأعداء أنواع وسلوكيات مختلفة، ونريد إضافة ترقية للقنبلة الذكية Smart Bomb التي تدمّر جميع الأعداء على الشاشة عند تنشيطها. هنا يمكننا الأمر باستخدام المجموعات بأقل قدر من الشيفرة البرمجية. سنحتاج أولًا إلى إضافة جميع الأعداء إلى المجموعة enemies في المحرّر باستخدام تبويب العقدة Node: يمكننا أيضًا إضافة عقد إلى المجموعة في السكربت الخاص بنا كما يلي: func _ready(): add_to_group("enemies") لنفترض أن لكل عدو الدالة explode() التي تتعامل مع ما يحدث عندما يموت، مثل تشغيل رسوم متحركة أو توليد عناصر متساقطة وما إلى ذلك. يمكننا الآن تنفيذ دالة القنبلة الذكية smart bomb الخاصة بنا كما يلي بعد أن أصبح كل عدو في مجموعته: func activate_smart_bomb(): get_tree().call_group("enemies", "explode") استخدام خاصية المالك owner owner هي خاصية عقدة Node التي تُضبَط تلقائيًا عند حفظ مشهد. تُضبَط هذه الخاصية لكل عقدة في هذا المشهد على العقدة الجذر للمشهد، مما يوفر طريقةً ملائمةً لتوصيل إشارات العقد الأبناء بالعقدة الرئيسية. مثال عن استخدام الخاصية owner سنحصل في أغلب الأحيان على تسلسل هرمي عميق ومتداخل من الحاويات وعناصر التحكم في واجهة مستخدم معقدة، وتصدر العقد التي يتفاعل معها المستخدم مثل عقدة Button إشاراتٍ. قد نرغب في ربط هذه الإشارات بالسكربت الموجود على عقدة الجذر لواجهة المستخدم. ليكن لدينا مثلًا الإعداد التالي: يحتوي السكربت الموجود على العقدة الجذر CenterContainer على الدالة التالية التي نريد استدعاءها عند الضغط على أيّ زر: extends CenterContainer func _on_button_pressed(button_name): print(button_name, " was pressed") تُعَد الأزرار هنا نسخًا من مشهد Button، وتمثّل كائنًا قد يحتوي على شيفرة برمجية ديناميكية تضبط نص الزر أو خاصيات أخرى؛ أو قد تكون لدينا أزرار تُضاف أو تُزال ديناميكيًا من الحاوية وفقًا لحالة اللعبة، ولكن ما نحتاجه لتوصيل إشارة الزر هو ما يلي: extends Button func _ready(): pressed.connect(owner._on_button_pressed.bind(name)) وكما هو ظاهر، ستبقى العقدة CenterContainer هي المالك owner بغض النظر عن المكان الذي تضع فيه الأزرار في الشجرة إذا أضفتَ مزيدًا من الحاويات مثلًا. ختامًا بهذا نكون قد تعرفنا على كيفية تحقيق تواصل سليم بين العقد Nodes في محرك الألعاب جودو Godot، وسنتابع في المقال التالي من هذه السلسلة في شرح مفهوم جديد من عالم الألعاب وهو delta. ترجمة -وبتصرّف- للقسم Node communication من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: ترتيب معالجة العقد والتنقل في شجرة المشاهد في Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot تعرف على واجهة محرك الألعاب جودو Godot
-
عند العمل مع قواعد البيانات العلاقية Relational Databases ولغة الاستعلام البنيوية SQL فإننا نجري معظم العمليات على البيانات الناتجة عن استعلامات منفَّذة صراحةً مثل استعلامات SELECT أو INSERT أو UPDATE. لكن يمكننا توجيه قواعد بيانات SQL لتنفيذ إجراءات مُعرَّفة مسبقًا تلقائيًا في كل مرة يقع فيها حدث معين باستخدام القوادح أو محفّزات التنفيذ Triggers. يمكننا مثلًا استخدام هذه القوادح للاحتفاظ بسجل يتضمن جميع تعليمات الحذف DELETE بحيث نحفظ بعد كل عملية حدث تقع تفاصيل هذه العملية ومن قام بها ومتى، كما يمكن استخدامها لتحديث البيانات التراكمية مثل المجموع أو المتوسط حيث يمكننا تحديث هذه البيانات الإحصائية كلما جرت عملية إضافة أو تحديث على البيانات الموجودة. سنستخدم في هذا المقال قوادح SQL مختلفة لتنفيذ الإجراءات تلقائيًا عندما ندرج الصفوف أو نحدثها أو نحذفها. مستلزمات العمل يجب توفر حاسوب يشغّل نظام إدارة قواعد بيانات علاقية RDBMS مستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذي صلاحيات مسؤول مختلف عن المستخدم الجذر، وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو، ومقال كيفية تثبيت توزيعة أوبنتو من لينكس بأبسط طريقة نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة التالية من المقال معرفة أساسية بتنفيذ استعلامات SELECT و INSERT و UPDATE و DELETE لمعالجة البيانات في قاعدة البيانات كما هو موضح في مقال كيفية الاستعلام عن السجلات من الجداول في SQL وكيفية إدراج البيانات في SQL وتحديث البيانات في لغة الاستعلام البنيوية SQL وحذف البيانات في لغة الاستعلام البنيوية SQL معرفة أساسية باستخدام الاستعلامات المتداخلة كما هو موضَّح في مقال كيفية استخدام الاستعلامات المتداخلة في لغة SQL المعرفة الأساسية باستخدام الدوال الرياضية التجميعية كما هو موضَّح في مقال كيفية استخدام التعابير الرياضية والدوال التجميعية في SQL ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL. فلا يفرض معيار SQL صيغةً للقوادح Triggers أو طريقة صارمة لتحقيقها بالرغم من أنها تُعَد جزءًا من هذا المعيار، لذا يختلف تقديمها من قاعدة البيانات إلى أخرى، وتستخدم الأوامر الموضَّحة في هذا المقال صيغة قاعدة بيانات MySQL وقد لا تعمل على محرّكات قواعد البيانات الأخرى. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لنتمكّن من التدرب على استخدام المحفّزات، وفي القسم التالي نوضح تفاصيل الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية لاستخدامها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل في هذا القسم بخادم MySQL وننشئ قاعدة بيانات تجريبية لتطبيق الأمثلة الواردة في هذا المقال، حيث سنستخدم قاعدة بيانات تحفظ مجموعة هدايا تذكارية افتراضية، ونخزّن كل تفاصيل الهدايا التذكارية المملوكة حاليًا، وقيمتها الإجمالية المتاحة ونحتاج للتأكد من أن إجراء حذف الهدايا التذكارية سيُحفَظ في سجل دائم يوضح كافة تفاصيل عملية الحذف. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نحتاج للاتصال بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم collectibles: mysql> CREATE DATABASE collectibles; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات collectibles من خلال تنفيذ تعليمة USE التالية: $ USE collectibles; وسيظهر الخرج التالي: Database changed اخترنا قاعدة البيانات، وسننشئ عدة جداول تجريبية ضمنها، حيث سيحتوي الجدول collectibles على بيانات مبسَّطة عن الهدايا التذكارية الموجودة في قاعدة البيانات، ويتضمن الجدول الأعمدة التالية: name: يخزّن اسم كل هدية تذكارية، ويستخدم نوع البيانات varchar بحد أقصى 50 محرفًا value: يخزّن قيمة الهدية التذكارية، ويستخدم نوع البيانات decimal بحد أقصى 5 قيم قبل الفاصلة العشرية وقيمتين بعدها أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE collectibles ( mysql> name varchar(50), mysql> value decimal(5, 2) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: Query OK, 0 rows affected (0.00 sec) سنسمّي الجدول الثاني باسم collectibles_stats وسنستخدمه لتتبّع القيمة المتراكمة لجميع الهدايا التذكارية في المجموعة، وسيحتوي هذا الجدول على صف واحد من البيانات مع الأعمدة التالية: count: يحتوي عدد الهدايا التذكارية المملوكة، ونمثّله باستخدام نوع البيانات int value: يخزّن القيمة المتراكمة لجميع الهدايا باستخدام نوع البيانات decimal بحد أقصى 5 قيم قبل الفاصلة العشرية وقيمتين بعدها أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE collectibles_stats ( mysql> count int, mysql> value decimal(5, 2) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: Query OK, 0 rows affected (0.00 sec) سنسمي الجدول الثالث والأخير بالاسم collectibles_archive، والذي سيتتبّع جميع الهدايا التذكارية المحذوفة من المجموعة لضمان عدم اختفائها أبدًا، وسيحتوي على بيانات مشابهة للجدول collectibles مع تاريخ الإزالة، وسيستخدم الأعمدة التالية: name: يحتوي اسم كل هدية تذكارية محذوفة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا value: يخزّن قيمة الهدايا التذكارية لحظة الحذف باستخدام نوع البيانات decimal بحد أقصى 5 قيم قبل الفاصلة العشرية وقيمتين بعدها removed_on: يخزّن تاريخ ووقت الحذف لكل هدية تذكارية مؤرشفة باستخدام نوع البيانات timestamp باستخدام القيمة الافتراضية NOW() التي تعني التاريخ الحالي لإدراج صف جديد في هذا الجدول أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE collectibles_archive ( mysql> name varchar(50), mysql> value decimal(5, 2), mysql> removed_on timestamp DEFAULT CURRENT_TIMESTAMP mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول collectibles_stats بالبيانات الأولية لمجموعة الهدايا التذكارية من خلال تنفيذ عملية INSERT INTO التالية: mysql> INSERT INTO collectibles_stats SELECT COUNT(name), SUM(value) FROM collectibles; تضيف عملية INSERT INTO السابقة صفًا واحدًا إلى الجدول collectibles_stats مع القيم المحسوبة باستخدام الدوال التجميعية Aggregate Functions لحساب عدد الصفوف في الجدول collectibles ولجمع قيم جميع الهدايا التذكارية باستخدام العمود value والدالة SUM. يشير الخرج التالي إلى إضافة الصف بنجاح: Query OK, 1 row affected (0.002 sec) Records: 1 Duplicates: 0 Warnings: 0 يمكننا التحقق من ذلك من خلال تنفيذ تعليمة SELECT التالية مع الجدول collectibles_stats: mysql> SELECT * FROM collectibles_stats; لا توجد هدايا تذكارية في قاعدة البيانات حتى الآن، لذا يكون العدد الأولي للعناصر هو 0 وتكون القيمة المتراكمة هي NULL كما يلي: +-------+-------+ | count | value | +-------+-------+ | 0 | NULL | +-------+-------+ 1 row in set (0.000 sec) نحن الآن جاهزون لمتابعة هذا المقال والبدء باستخدام القوادح Triggers في MySQL. فهم Triggers القوادح Triggers هي تعليمات مُعرَّفة من أجل جدول معين تنفّذها قاعدة البيانات تلقائيًا في كل مرة يقع فيها حدث محدد في هذا الجدول، ويمكننا استخدامها لضمان تنفيذ بعض الإجراءات بتناسق في كل مرة نُنفَّذ فيها تعليمة معينة مع الجدول بدلًا من أن يحتاج مستخدمو قاعدة البيانات لتنفيذها يدويًا. يُعرَّف كل Trigger مرتبط بجدول باسم يحدده المستخدم، وشرطين لتوجيه محرك قاعدة البيانات وإعلامه بالوقت المناسب لتنفيذ القادح، ويمكن تجميع هذين الشرطين ضمن فئتين منفصلتين هما: حدث قاعدة البيانات: يمكن تنفيذ القوادح عند تشغيل تعليمات INSERT أو UPDATE أو DELETE مع الجدول وقت الحدث: يمكن تنفيذ القوادح أيضًا قبل BEFORE أو بعد AFTER التعليمة المحدَّدة يؤدي الجمع بين مجموعتي الشروط السابقتين لستة احتمالات منفصلة للقوادح التي تُنفَّذ تلقائيًا في كل مرة يتحقق فيها الشرط المشترك. القوادح التي تحدث قبل تنفيذ التعليمة التي تحقق الشرط هي BEFORE INSERT و BEFORE UPDATE و BEFORE DELETE، ويمكن استخدامها لمعالجة البيانات والتحقق من صحتها قبل إدراجها أو تحديثها في الجدول أو لحفظ تفاصيل الصف المحذوف لأغراض التدقيق أو الأرشفة. المحفّزات التي تحدث بعد تنفيذ التعليمة التي تحقق الشرط هي AFTER INSERT و AFTER UPDATE و AFTER DELETE، ويمكن استخدامها لتحديث القيم الملخَّصة في جدول منفصل بناءً على الحالة النهائية لقاعدة البيانات بعد التعليمة. يمكن تنفيذ إجراءات مثل التحقق من صحة بيانات الدخل، ومعالجتها، أو أرشفة الصف المحذوف، إذ تسمح قاعدة البيانات بالوصول إلى قيم البيانات من داخل القوادح، ويمكن استخدام البيانات المدرَجة حديثًا فقط بالنسبة لقوادح INSERT، ويمكن الوصول إلى كلٍّ من البيانات الأصلية والمُحدَّثة بالنسبة لمحفّزات UPDATE، وتكون بيانات الصف الأصلية فقط متاحة للاستخدام بالنسبة لمحفّزات DELETE نظرًا لعدم وجود بيانات جديدة للإشارة إليها. يمكن الوصول للبيانات المُستخدَمة في جسم القادح ضمن السجل OLD بالنسبة للبيانات الموجودة حاليًا في قاعدة البيانات والسجل NEW بالنسبة للبيانات التي سيحفظها الاستعلام، ويمكن الإشارة إلى الأعمدة الفردية باستخدام الصيغة OLD.column_name و OLD.column_name. يوضّح المثال التالي الصيغة العامة لتعليمة SQL المُستخدَمة لإنشاء قادح جديد: mysql> CREATE TRIGGER trigger_name trigger_condition mysql> ON table_name mysql> FOR EACH ROW mysql> trigger_actions; لنشرح التعليمة السابقة بالتفصيل: CREATE TRIGGER: اسم تعليمة SQL المُستخدَمة لإنشاء قادح جديد في قاعدة البيانات trigger_name: هو الاسم الذي يحدّده المستخدم للقادح، ويصف دوره مثل استخدام أسماء الجداول وأسماء الأعمدة لوصف معناها ON table_name: نخبر قاعدة البيانات بأن القادح يجب أن يراقب الأحداث التي تحدث في الجدول table_name trigger_condition: أحد الاختيارات الستة المُحتملة التي تحدد متى يجب تشغيل القادح مثل BEFORE INSERT. FOR EACH ROW: تخبر قاعدة البيانات بأنه يجب تشغيل القادح لكل صف يتأثر بالحدث. تدعم بعض قواعد البيانات أنماطًا إضافية للتنفيذ مختلف عن النمط FOR EACH ROW، ولكن تشغيل التعليمات من جسم القادح لكل صف متأثر بالتعليمة التي تسبّبت في تنفيذ القادح هو الخيار الوحيد في حال MySQL trigger_actions: جسم القادح الذي يحدّد ما يحدث عند تنفيذه، وهو تعليمة SQL واحدة، ويمكن تضمين تعليمات متعددة في جسم القادح لإجراء عمليات معقدة باستخدام الكلمات المفتاحية BEGIN و END لتضمين التعليمات ضمن كتلة، ولكن ذلك خارج نطاق هذا المقال اطّلع على التوثيق الرسمي للمحفّزات لمعرفة المزيد حول الصيغة المُستخدمَة لتعريف القوادح. سننشئ في القسم التالي أمثلة على قوادح تعالج البيانات قبل إجراء عمليتي INSERT و UPDATE. معالجة البيانات باستخدام محفزات BEFORE INSERT و BEFORE UPDATE سنستخدم في هذا القسم قوادح لمعالجة البيانات قبل تنفيذ تعليمات INSERT و UPDATE، حيث سنستخدم المحفّزات في للتأكّد من أن جميع الهدايا التذكارية في قاعدة البيانات تستخدم أسماءً بحروف كبيرة لتحقيق التناسق. في حال لم نستخدم قوادح سيتوجب علينا تذكّر استخدام أسماء الهدايا التذكارية بحروف كبيرة لكل تعليمة INSERT و UPDATE، وإذا نسينا، فستحتفظ قاعدة البيانات بالبيانات كما هي وهذا قد يؤدي إلى حدوث أخطاء محتملة في مجموعة البيانات. لنبدأ بإدخال مثال لعنصر من الهدايا التذكارية بالاسم spaceship model وبقيمة 12.50 دولار، وسنكتب اسم العنصر بحروف صغيرة لتوضيح المشكلة. لننفّذ التعليمة التالية: mysql> INSERT INTO collectibles VALUES ('spaceship model', 12.50); تؤكد الرسالة التالية إضافة العنصر: Query OK, 1 row affected (0.009 sec) يمكننا التحقق من إدراج الصف من خلال تنفيذ استعلام SELECT التالي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | spaceship model | 12.50 | +-----------------+-------+ 1 row in set (0.000 sec) حُفِظ هذا العنصر كما هو مع كتابة اسمه بحروف صغيرة فقط. لنتأكد من كتابة جميع الهدايا التذكارية اللاحقة بحروف كبيرة دائمًا من خلال إنشاء قادح باسم BEFORE INSERT لمعالجة البيانات المُمرَّرة إلى قاعدة البيانات قبل حدوثها. نشغّل الآن التعليمة التالية: mysql> CREATE TRIGGER uppercase_before_insert BEFORE INSERT mysql> ON collectibles mysql> FOR EACH ROW mysql> SET NEW.name = UPPER(NEW.name); ينشئ الأمر السابق قادح باسم uppercase_before_insert، والذي سيُنفَّذ قبل كافة تعليمات INSERT في الجدول collectibles. تُنفَّذ التعليمة الموجودة في القادحSET NEW.name = UPPER(NEW.name) لكل صف مدرج، ويسند أمر SQL الذي هو SET القيمة الموجودة على الجانب الأيمن إلى الجانب الأيسر، حيث يمثل NEW.name قيمة العمود name الذي ستحفظه تعليمة الإدراج. نحوّل حالة الحروف للقيمة التي ستُحفَظ في قاعدة البيانات من خلال تطبيق الدالة UPPER على اسم الهدية وإسناده مرة أخرى لقيمة العمود. ملاحظة: قد تظهر رسالة خطأ مشابهة للخطأ التالي عند تشغيل الأمر CREATE TRIGGER. ERROR 1419 (HY000): You do not have the SUPER privilege, and binary logging is enabled (you might want to use the less safe log_bin_trust_function_creators variable) التسجيل الثنائي Binary Logging هو آلية تُسجل كل التعديلات التي تتم على قاعدة البيانات مثل إضافة أو تعديل أو حذف بيانات في سجل ثنائي، ويحتوي هذا السجل على أحداث Events تصف التعديلات التي حدثت وتحفظها بتنسيق ثنائي يمكن معالجته، وهذا التسجيل يكون مفعَّلًا افتراضيًا في محرّك قاعدة بيانات MySQL وذلك بدءًا من الإصدار MySQL 8، حيث يتعقّب التسجيل الثنائي جميع تعليمات SQL التي تعدّل محتويات قاعدة البيانات في صيغة أحداث محفوظة تَصِف هذه التعديلات، وتُستخدَم هذه السجلات في النسخ المتماثل Replication لقاعدة البيانات للحفاظ على مزامنة النسخ المتماثلة لقاعدة البيانات وأثناء استعادة البيانات في الوقت المناسب. لكن لا يسمح MySQL بإنشاء القوادح Triggers والإجراءات المخزَّنة Stored Procedures في حال تفعيل تسجيل التعديلات أو الأحداث بصيغة ثنائية كإجراء احترازي لضمان سلامة البيانات وتكاملها في بيئات النسخ المتماثل Replication، ولكن فهم كيفية تأثير القوادح والإجراءات المُخزَّنة على النسخ المتماثل خارج نطاق هذا المقال. يمكننا تجاوز القيود التي يفرضها MySQL عند تفعيل التسجيل الثنائي Binary Logging، وذلك لأغراض التعلم أو الاختبار في بيئة محلية على جهازنا الشخصي، ولكن لن يستمر هذا الإعداد الذي عدّلناه بالعمل وسيعود للقيمة الأصلية عند إعادة تشغيل خادم MySQL. يمكن تجاوز الإعداد الافتراضي من خلال تعديل الإعدادات الخاصة بـ MySQL كما يلي: mysql> SET GLOBAL log_bin_trust_function_creators = 1; يتحكم الإعداد log_bin_trust_function_creators بإمكانية الوثوق بالمستخدمين الذين ينشئون القوادح والدوال المخزنة بحيث لا ينشئون قوادح تتسبب في كتابة أحداث غير آمنة في السجل الثنائي. تكون قيمة الإعداد الافتراضية هي 0، مما يسمح للمستخدمين الذين يتمتعون بصلاحيات مميزة فقط بإنشاء قوادح في البيئة التي فعّلنا فيها تسجيل التعديلات أو الأحداث بصيغة ثنائية، وإذا عدّلنا القيمة إلى 1، فسنثق بأيّ مستخدم ينشئ تعليمات CREATE TRIGGER لفهم النتائج. جرى الآن تحديث الإعداد، لنعمل إذًا على تسجيل الخروج كمستخدم جذر، وتسجيل الدخول مرة أخرى كمستخدم عادي، ونعيد تشغيل تعليمة CREATE TRIGGER. ويمكن الاطلاع على توثيق MySQL الرسمي: السجل الثنائي و تسجيل التعديلات أو الأحداث بصيغة ثنائية للبرنامج المخزن، كما يمكن مطالعة مقال كيفية إعداد النسخ المتماثل في MySQL لمعرفة المزيد حول تسجيل التعديلات أو الأحداث بصيغة ثنائية والنسخ المتماثل في MySQL وارتباطه بالقوادح. ملاحظة: قد نتلقى خطأً عند تنفيذ أمر CREATE TRIGGER اعتمادًا على أذونات مستخدم MySQL الخاصة بنا ERROR 1142 (42000): TRIGGER command denied to user 'user'@'host' for table 'collectibles' لحل هذا الخطأ يمكن منح أذونات TRIGGER للمستخدم الخاص بنا من خلال تسجيل الدخول إلى MySQL كمستخدم جذر، وتنفيذ الأوامر التالية مع وضع اسم مستخدم MySQL والمضيف حسب الحاجة: mysql> GRANT TRIGGER on *.* TO 'user'@'localhost'; mysql> FLUSH PRIVILEGES; نحدّث أذونات المستخدم، ثم نسجّل الخروج كمستخدم جذر، ونسجّل الدخول مرة أخرى كمستخدم عادي، ونعيد تشغيل التعليمة CREATE TRIGGER، وسيطبع MySQL الرسالة التالية للتأكد من إنشاء القادح بنجاح: Query OK, 1 row affected (0.009 sec) نحاول الآن إدراج مجموعة هدايا تذكارية جديدة باستخدام وسيط بحروف صغيرة مع استعلام INSERT كما يلي: mysql> INSERT INTO collectibles VALUES ('aircraft model', 10.00); ثم نتحقّق من الصفوف الناتجة في الجدول collectibles كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | spaceship model | 12.50 | | AIRCRAFT MODEL | 10.00 | +-----------------+-------+ 2 rows in set (0.000 sec) يشير الإدخال الجديد هذه المرة إلى أن AIRCRAFT MODEL -مع جميع حروفه الكبيرة- يختلف عن الإدخال الذي حاولنا إدراجه، حيث شُغِّل القادح في الخلفية وحوّل حالة الحروف قبل حفظ الصف في قاعدة البيانات. يحمي القادح جميع الصفوف الجديدة لضمان حفظ الأسماء بحروف كبيرة، ولكن لا يزال من الممكن حفظ البيانات غير المقيَّدة التي تستخدم تعليمات UPDATE، حيث يمكن حماية تعليمات UPDATE باستخدام التأثير نفسه، إذًا لننشئ قادحًا آخر كما يلي: mysql> CREATE TRIGGER uppercase_before_update BEFORE UPDATE mysql> ON collectibles mysql> FOR EACH ROW mysql> SET NEW.name = UPPER(NEW.name); يكمن الفرق بين القادحين في المعايير، فالقادح هنا BEFORE UPDATE، مما يعني تنفيذه في كل مرة تُنفَّذ فيها تعليمة UPDATE مع الجدول، ويؤثر ذلك على الصفوف الموجودة في كل تحديث، بالإضافة إلى الصفوف الجديدة التي يؤثر عليها القادح السابق. سيعطي MySQL تأكيدًا بإنشاء القادح بنجاح كما يلي: Query OK, 0 row affected (0.009 sec) يمكن التحقق من سلوك القادح الجديد من خلال تحديث قيمة سعر spaceship model كما يلي: mysql> UPDATE collectibles SET value = 15.00 WHERE name = 'spaceship model'; ترشّح تعليمة WHERE الصف المراد تحديثه حسب الاسم، وتغيّر تعليمة SET القيمة إلى 15.00، وسيظهر الخرج التالي، مما يؤكد أن التعليمة قد غيّرت صفًا واحدًا: Query OK, 1 row affected (0.002 sec) Rows matched: 1 Changed: 1 Warnings: 0 لنتحقّق الآن من الصفوف الناتجة في الجدول collectibles كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | SPACESHIP MODEL | 15.00 | | AIRCRAFT MODEL | 10.00 | +-----------------+-------+ 2 rows in set (0.000 sec) أصبح الاسم الآن SPACESHIP MODEL بالإضافة إلى تحديث السعر إلى 15.00 باستخدام التعليمة المُنفَّذة. إذا شغّلنا تعليمة UPDATE، فسيُنفَّذ القادح، مما يؤثر على القيم الموجودة في الصف المُحدَّث، مع تحويل عمود الاسم إلى حروف كبيرة قبل الحفظ. أنشأنا في هذا القسم قادحين Triggersيعملان قبل استعلامات INSERT وقبل استعلامات UPDATE لجعل البيانات ملائمةً قبل حفظها في قاعدة البيانات، وسنستخدم في القسم التالي محفّزات BEFORE DELETE لنسخ الصفوف المحذوفة في جدول منفصل للأرشفة. استخدام قوادح BEFORE DELETE قد نرغب في أرشفة البيانات بدلاً من حذفها نهائيًا من قاعدة البيانات، خاصةً إذا كنا بحاجة للاحتفاظ بسجل لهذه البيانات في المستقبل، فإذ أنشأنا في بداية هذا المقال جدول آخر باسم collectibles_archive لتعقّب جميع الهدايا التذكارية المحذوفة من المجموعة وأرشفتها. سنستخدم قادح يُنفَّذ قبل تنفيذ تعليمات الحذف DELETE. نتحقّق أولًا مما إذا كان جدول الأرشيف فارغًا بالكامل من خلال تنفيذ التعليمة التالية: mysql> SELECT * FROM collectibles_archive; وسيظهر الخرج التالي، مما يؤكد أن الجدول collectibles_archive فارغ: Empty set (0.000 sec) إذا نفّذنا استعلام DELETE مع الجدول collectibles، فيمكن حذف أيّ صفٍ من الجدول دون أيّ أثر. يمكن معالجة ذلك من خلال إنشاء قادح يُنفَّذ قبل جميع استعلامات DELETE مع الجدول collectibles، والغرض من هذا القادح هو حفظ نسخة من الكائن المحذوف في جدول الأرشيف قبل حدوث الحذف. لنشغّل الآن الأمر التالي: mysql> CREATE TRIGGER archive_before_delete BEFORE DELETE mysql> ON collectibles mysql> FOR EACH ROW mysql> INSERT INTO collectibles_archive (name, value) VALUES (OLD.name, OLD.value); يُسمَّى هذا القادح باسم archive_before_delete ويحدث قبل أيّ استعلامات DELETE مع الجدول collectibles. ستُنفَّذ تعليمة INSERT لكل صفٍ سيُحذف، بينما تدرج تعليمة INSERT صفًا جديدًا في الجدول collectibles_archive مع قيم البيانات المأخوذة من السجل OLD، وهو السجل المقرّر حذفه، حيث يصبح OLD.name هو العمود name ويصبح OLD.value العمود value. وتؤكد قاعدة البيانات إنشاء هذا القادح كما يلي: Query OK, 0 row affected (0.009 sec) نحاول حذف إحدى الهدايا التذكارية من الجدول collectibles الرئيسي مع استخدام القادح كما يلي: mysql> DELETE FROM collectibles WHERE name = 'SPACESHIP MODEL'; ويؤكّد الخرج التالي نجاح تشغيل الاستعلام السابق: Query OK, 1 row affected (0.004 sec) لنسرد الآن جميع الهدايا التذكارية كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +----------------+-------+ | name | value | +----------------+-------+ | AIRCRAFT MODEL | 10.00 | +----------------+-------+ 1 row in set (0.000 sec) حذفنا SPACESHIP MODEL ولم يَعُد موجودًا في الجدول مع بقاء AIRCRAFT MODEL، ولكن يجب تسجيل هذا الحذف في الجدول collectibles_archive باستخدام القادح الذي أنشأناه مسبقًا، إذًا لنتحقق من ذلك، ولننفّذ استعلامًا آخر كما يلي: mysql> SELECT * FROM collectibles_archive; وسيظهر الخرج التالي: +-----------------+-------+---------------------+ | name | value | removed_on | +-----------------+-------+---------------------+ | SPACESHIP MODEL | 15.00 | 2022-11-20 11:32:01 | +-----------------+-------+---------------------+ 1 row in set (0.000 sec) لاحظ القادح عملية الحذف في هذا الجدول تلقائيًا، مع تعبئة أعمدة name و value بالبيانات من الصف المحذوف، ولم يضبط القادح العمود الثالث removed_on صراحةً، لذلك سيأخذ القيمة الافتراضية المُحدَّدة أثناء إنشاء الجدول، أي تاريخ إنشاء أيّ صف جديد، مما يؤدي دائمًا إلى إضافة تعليق توضيحي لكل إدخال مُضاف بمساعدة القادح مع تاريخ الحذف. يمكننا الآن مع وجود هذا القادح التأكد من أن جميع استعلامات DELETE ستؤدي إلى إدخال سجلٍ في الجدول collectibles_archive مع ترك معلومات حول الهدايا التذكارية المملوكة مسبقًا. سنستخدم في القسم التالي القوادح التي تُنفَّذ بعد تعليمات تحديث الجدول الذي يحتوي على القيم المُجمَّعة بناءً على جميع الهدايا التذكارية. استخدام محفزات AFTER INSERT و AFTER UPDATE و AFTER DELETE استخدمنا في القسمين السابقين القوادح المُنفَّذة قبل التعليمات الرئيسية لتنفيذ العمليات المستندة إلى البيانات الأصلية قبل تحديث قاعدة البيانات، وسنحدّث في هذا القسم جدول البيانات التلخيصية بالعدد والقيمة المتراكمة لجميع الهدايا التذكارية المحدَّثة دائمًا باستخدام القوادح المُنفَّذة بعد التعليمات المطلوبة، وبذلك سنكون متأكدين من أن بيانات الجدول تأخذ في الحسبان الحالة الحالية لقاعدة البيانات. لنبدأ الآن بفحص الجدول collectibles_stats كما يلي: mysql> SELECT * FROM collectibles_stats; لم نضف معلومات إلى هذا الجدول بعد، لذا يكون عدد عناصر الهدايا التذكارية المملوكة هو 0، والقيمة التراكمية هي NULL: +-------+-------+ | count | value | +-------+-------+ | 0 | NULL | +-------+-------+ 1 row in set (0.000 sec) لا توجد قوادح لهذا الجدول، وبالتالي لم تؤثر الاستعلامات الصادرة مسبقًا لإدراج الهدايا التذكارية وتحديثها على هذا الجدول. نريد ضبط القيم في صفٍ واحد من الجدول collectibles_stats لتقديم معلومات مُحدَّثة حول عدد الهدايا التذكارية وقيمتها الإجمالية، حيث نتأكد من تحديث محتويات الجدول بعد كل عملية إدراج INSERT أو تحديث UPDATE أو حذف DELETE من خلال إنشاء ثلاثة قوادح منفصلة وتنفيذها بعد الاستعلام المقابل لها. لننشئ أولًا القادح AFTER INSERT: mysql> CREATE TRIGGER stats_after_insert AFTER INSERT mysql> ON collectibles mysql> FOR EACH ROW mysql> UPDATE collectibles_stats mysql> SET count = ( mysql> SELECT COUNT(name) FROM collectibles mysql> ), value = ( mysql> SELECT SUM(value) FROM collectibles mysql> ); سمّينا القادح بالاسم stats_after_insert وسيُنفَّذ بعد AFTER كل استعلام INSERT في الجدول collectibles مع تشغيل تعليمة UPDATE في جسم القادح. يؤثر استعلام UPDATE على جدول البيانات التلخيصية ويضبط العمودين count و value على القيم التي تعيدها الاستعلامات المتداخلة كما يلي: SELECT COUNT(name) FROM collectibles: يحصل على عدد الهدايا التذكارية SELECT SUM(value) FROM collectibles: يحصل على القيمة الإجمالية لجميع الهدايا التذكارية وتؤكّد قاعدة البيانات إنشاء القادح كما يلي: Query OK, 0 row affected (0.009 sec) نجرّب الآن إعادة إدراج spaceship model المحذوف مسبقًا في الجدول collectibles للتحقق من التحديث الصحيح لجدول البيانات التلخيصية كما يلي: mysql> INSERT INTO collectibles VALUES ('spaceship model', 15.00); وتطبع قاعدة البيانات رسالة النجاح التالية: Query OK, 1 row affected (0.009 sec) يمكن سرد جميع الهدايا التذكارية المملوكة باستخدام الاستعلام التالي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | AIRCRAFT MODEL | 10.00 | | SPACESHIP MODEL | 15.00 | +-----------------+-------+ 2 rows in set (0.000 sec) يوجد نوعان من عناصر الهدايا التذكارية التي تبلغ قيمتها الإجمالية 25.00. لنفحص الآن جدول البيانات التلخيصية بعد العنصر الذي أدرجناه حديثًا من خلال تنفيذ الاستعلام التالي: mysql> SELECT * FROM collectibles_stats; سيسرد الجدول هذه المرة عدد جميع عناصر الهدايا التذكارية المملوكة التي هي 2 وقيمتها التراكمية 25.00، والتي تطابق الخرج السابق: +-------+-------+ | count | value | +-------+-------+ | 2 | 25.00 | +-------+-------+ 1 row in set (0.000 sec) يُنفَّذ القادح stats_after_insert بعد استعلام INSERT ويحدّث الجدول collectibles_stats بالبيانات الحالية count و value للمجموعة، وتُجمَع إحصائيات محتويات المجموعة بأكملها، وليس الإدخال الأخير فقط. تحتوي المجموعة الآن على عنصرين spaceship model و aircraft model، لذا يسرد جدول البيانات التلخيصية عنصرين مع قيمتهما الإجمالية، وبالتالي ستؤدي إضافة أيّ عنصر هدية تذكارية جديد إلى الجدول collectibles إلى تحديث جدول البيانات التلخيصية بالقيم الصحيحة، ولكن لن يؤثر تحديث العناصر الموجودة أو حذف الهدايا التذكارية على جدول البيانات التلخيصية أبدًا، لذا سننشئ قادحين إضافيين لإجراء عمليات متطابقة ولكن تحفّزها أحداث مختلفة كما يلي: mysql> CREATE TRIGGER stats_after_update AFTER UPDATE mysql> ON collectibles mysql> FOR EACH ROW mysql> UPDATE collectibles_stats mysql> SET count = ( mysql> SELECT COUNT(name) FROM collectibles mysql> ), value = ( mysql> SELECT SUM(value) FROM collectibles mysql> ); mysql> mysql> CREATE TRIGGER stats_after_delete AFTER DELETE mysql> ON collectibles mysql> FOR EACH ROW mysql> UPDATE collectibles_stats mysql> SET count = ( mysql> SELECT COUNT(name) FROM collectibles mysql> ), value = ( mysql> SELECT SUM(value) FROM collectibles mysql> ); أنشأنا قادحين جديدين هما: stats_after_update و stats_after_delete، وسيُنفّذان مع الجدول collectible_stats عند تنفيذ تعليمة UPDATE أو DELETE في الجدول collectibles. يؤدي الإنشاء الناجح لهذين القادحين إلى طباعة الخرج التالي: Query OK, 0 row affected (0.009 sec) لنحدّث الآن قيمة السعر لإحدى الهدايا التذكارية كما يلي: mysql> UPDATE collectibles SET value = 25.00 WHERE name = 'AIRCRAFT MODEL'; ترشّح تعليمة WHERE الصف المراد تحديثه حسب الاسم، وتغيّر تعليمة SET القيمة إلى 25.00. يؤكّد الخرج التالي أن التعليمة غيّرت صفًا واحدًا: Query OK, 1 row affected (0.002 sec) Rows matched: 1 Changed: 1 Warnings: 0 لنتحقق مرة أخرى من محتويات جدول البيانات التلخيصية بعد التحديث كما يلي: mysql> SELECT * FROM collectibles_stats; وسيسرد العمود value الآن القيمة 40.00، وهي القيمة الصحيحة بعد التحديث: +-------+-------+ | count | value | +-------+-------+ | 2 | 40.00 | +-------+-------+ 1 row in set (0.000 sec) الخطوة الأخيرة هي التحقق من أن جدول البيانات التلخيصية سيظهِر حذف إحدى الهدايا التذكارية بطريقة صحيحة، فلنحاول حذف العنصر aircraft model كما يلي: mysql> DELETE FROM collectibles WHERE name = 'AIRCRAFT MODEL'; ويؤكد الخرج التالي تشغيل الاستعلام بنجاح: Query OK, 1 row affected (0.004 sec) لنسرد الآن جميع الهدايا التذكارية كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | SPACESHIP MODEL | 15.00 | +-----------------+-------+ 1 row in set (0.000 sec) لاحظ بقاء العنصر SPACESHIP MODEL فقط. لنتحقق الآن من القيم الموجودة في جدول البيانات التلخيصية كما يلي: mysql> SELECT * FROM collectibles_stats; وسيظهر الخرج التالي: +-------+-------+ | count | value | +-------+-------+ | 1 | 15.00 | +-------+-------+ 1 row in set (0.000 sec) يعرض العمود count الآن هدية تذكارية واحدة فقط في الجدول الرئيسي، والقيمة الإجمالية هي 15.00، وهي مطابقة لقيمة العنصر SPACESHIP MODEL. تعمل المحفّزات الثلاثة السابقة مع بعضها البعض بعد استعلامات INSERT و UPDATE و DELETE للحفاظ على مزامنة جدول الملخص مع القائمة الكاملة للهدايا التذكارية. سنتعلم في القسم التالي كيفية معالجة القوادح الموجودة مسبقًا مع قاعدة البيانات. سرد وحذف القوادح Triggers أنشأنا في الأقسام السابقة قوادح جديدة، ولكن يمكنك أيضًا سردها ومعالجتها عند الحاجة لأنها كائنات مسمَّاة ومُعرَّفة في قاعدة البيانات مثل الجداول، حيث يمكنك سرد جميع القوادح من خلال تنفيذ التعليمة SHOW TRIGGERS كما يلي: mysql> SHOW TRIGGERS; وسيتضمن الخرج جميع القوادح مع أسمائها وحدث التحفيز مع الوقت قبل BEFORE أو بعد AFTER تنفيذ التعليمة، بالإضافة إلى التعليمات التي تشكّل جزءًا من جسم القادح والتفاصيل الشاملة الأخرى لتعريفه كما يلي: +-------------------------+--------+--------------+--------(...)+--------+(...) | Trigger | Event | Table | Statement | Timing |(...) +-------------------------+--------+--------------+--------(...)+--------+(...) | uppercase_before_insert | INSERT | collectibles | SET (...)| BEFORE |(...) | stats_after_insert | INSERT | collectibles | UPDATE (...)| AFTER |(...) | uppercase_before_update | UPDATE | collectibles | SET (...)| BEFORE |(...) | stats_after_update | UPDATE | collectibles | UPDATE (...)| AFTER |(...) | archive_before_delete | DELETE | collectibles | INSERT (...)| BEFORE |(...) | stats_after_delete | DELETE | collectibles | UPDATE (...)| AFTER |(...) +-------------------------+--------+--------------+--------(...)+--------+(...) 6 rows in set (0.001 sec) يمكن حذف القوادح الموجودة مسبقًا من خلال استخدام تعليمات SQL التي هي DROP TRIGGER، فمثلًا إن لم نعد نرغب بفرض الحروف الكبيرة على أسماء الهدايا التذكارية، ولم نعد بحاجة إلى القادحين uppercase_before_insert و uppercase_before_update، فيمكننا إزالتهما من خلال تنفيذ الأمرين التالية: mysql> DROP TRIGGER uppercase_before_insert; mysql> DROP TRIGGER uppercase_before_update; ويستجيب MySQL برسالة النجاح التالية: Query OK, 0 rows affected (0.004 sec) لنجرّب الآن إضافة هدية تذكارية جديدة بحروف صغيرة كما يلي: mysql> INSERT INTO collectibles VALUES ('ship model', 10.00); وستؤكد قاعدة البيانات ذلك كما يلي: Query OK, 1 row affected (0.009 sec) يمكننا التحقق من إدراج الصف من خلال تنفيذ استعلام SELECT التالي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | SPACESHIP MODEL | 15.00 | | ship model | 10.00 | +-----------------+-------+ 2 rows in set (0.000 sec) نلاحظ كتابة الهدية التذكارية المضافة حديثًا بحروف صغيرة، إذًا لم يتغيّر الاسم عن الخرج الأصلي، وبالتالي تأكّدنا من أن القادح الذي حوّل حالة الحروف سابقًا لم يَعُد قيد الاستخدام. الخلاصة تعلمنا في هذا المقال ما هي القوادح Triggers في SQL وكيفية استخدامها في MySQL لمعالجة البيانات قبل استعلامات INSERT و UPDATE، وتعلّمنا كيفية استخدام قادح BEFORE DELETE لأرشفة الصف المحذوف في جدول منفصل، بالإضافة إلى استخدام قوادح AFTER بعد التعليمات لإبقاء جدول البيانات التلخيصية مُحدَّثة باستمرار. يمكننا استخدام الدوال لتفريغ بعض عمليات معالجة البيانات والتحقق من صحتها في محرّك قاعدة البيانات، مما يضمن سلامة البيانات أو إخفاء بعض سلوكيات قاعدة البيانات عن المستخدم الذي يستخدم قاعدة البيانات يوميًا، وقد غطّى هذا المقال أساسيات استخدام القوادح لهذا الغرض فقط، ولكن يمكنك أيضًا إنشاء قوادح معقدة تتكوّن من تعليمات متعددة واستخدام المنطق الشرطي لتنفيذ الإجراءات بدقة أكبر. ويمكن مطالعة توثيق MySQL الخاص بالتعامل مع triggers لمزيد من المعلومات. ترجمة -وبتصرف- للمقال How To Use Triggers in MySQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: استخدام الدوال Functions في لغة SQL التعامل مع قواعد البيانات مدخل إلى تصميم قواعد البيانات استخدام الدوال Functions في لغة SQL استخدام العروض Views في لغة الاستعلام البنيوية SQL
-
لا يقتصر التعامل في أنظمة إدارة قواعد البيانات العلاقية Relational Database Management System ولغة الاستعلام البنيوية SQL على تخزين البيانات وإدارتها واسترجاعها من قاعدة البيانات. إذ يمكن للغة SQL أيضًا إجراء عمليات حسابية ومعالجة البيانات عن طريق الدوال Functions، حيث يمكننا على سبيل المثال استخدام دوال محددة لاسترجاع أسعار المنتجات مع تقريب أسعارها، أو حساب متوسط عدد عمليات شراء منتج ما، أو تحديد عدد أيام انتهاء صلاحية الضمان على عملية شراء معينة. سنستخدم في هذا المقال دوالًا مختلفة في لغة SQL من أجل إجراء عمليات حسابية رياضية، ومعالجة السلاسل النصية والتواريخ، وحساب إحصائيات باستخدام الدوال التجميعية Aggregate Functions. مستلزمات العمل يجب أن يكون لديك حاسوب يشغّل نظام إدارة قواعد بيانات علاقية RDBMS مستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذو صلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضّح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال من خلال مستخدم مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة التالية من المقال معرفة أساسية بتنفيذ استعلامات SELECT لاختيار البيانات من قاعدة البيانات كما هو موضّح في مقال كيفية الاستعلام عن السجلات من الجداول في SQL ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن تحدّد صيغة SQL المعيارية عددًا محدودًا من الدوال، ويختلف دعم الصيغة المعيارية بين محرّكات قواعد البيانات المختلفة، لذا قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لنتمكّن من التدرب على استخدام الدوال، ويمكن مطالعة القسم التالي للحصول على تفاصيل حول الاتصال بخادم MySQL وإنشاء قاعدة البيانات التجريبية التي سنستخدمها في أمثلتنا. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل في هذا القسم بخادم MySQL وننشئ قاعدة بيانات تجريبية لنتمكّن من اتباع الأمثلة الواردة في هذا المقال. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نتصل بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشِئ قاعدة بيانات بالاسم bookstore: mysql> CREATE DATABASE bookstore; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات bookstore من خلال تنفيذ تعليمة USE التالية: $ USE bookstore; وسيَظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، وسننشئ عدة جداول تجريبية ضمنها، حيث سنستخدم مكتبة افتراضية تبيع كتبًا لعدة مؤلفين. سيحتوي الجدول inventory على بيانات كتب ممثّلة باستخدام الأعمدة التالية: book_id: المعرّف الخاص بكل كتاب، ونمثّله بنوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي Primary Key للجدول، حيث تصبح كل قيمة معرّفًا فريدًا للصف الخاص بها author: اسم مؤلف الكتاب، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا title: عنوان الكتاب الذي جرى شراؤه، ونمثّله باستخدام نوع بيانات varchar بحد أقصى 200 محرف introduction_date: تاريخ تقديم المكتبة لكل كتاب، ونمثّله باستخدام نوع البيانات date stock: يحتوي هذا العمود على عدد الكتب الموجودة في مخزن المكتبة، ونمثّله باستخدام نوع البيانات int price: سعر الكتاب، ونمثّله باستخدام نوع البيانات decimal بحد أقصى 5 قيم قبل الفاصلة العشرية وقيمتين بعدها ننشئ هذا الجدول التجريبي باستخدام الأمر التالي: CREATE TABLE inventory ( mysql> book_id int, mysql> author varchar(50), mysql> title varchar(200), mysql> introduction_date date, mysql> stock int, mysql> price decimal(5, 2), mysql> PRIMARY KEY (book_id) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول الأول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل جدول المشتريات ببعض البيانات التجريبية من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO inventory mysql> VALUES mysql> (1, 'Oscar Wilde', 'The Picture of Dorian Gray', '2022-10-01', 4, 20.83), mysql> (2, 'Jane Austen', 'Pride and Prejudice', '2022-10-04', 12, 42.13), mysql> (3, 'Herbert George Wells', 'The Time Machine', '2022-09-23', 7, 21.99), mysql> (4, 'Mary Shelley', 'Frankenstein', '2022-07-23', 9, 17.43), mysql> (5, 'Mark Twain', 'The Adventures of Huckleberry Finn', '2022-10-01', 14, 23.15); ستضيف عملية INSERT INTO خمسة كتب إلى الجدول inventory، حيث يشير الخرج التالي إلى إضافة جميع الصفوف الخمسة بنجاح: الخرج Query OK, 5 rows affected (0.00 sec) Records: 5 Duplicates: 0 Warnings: 0 نحن الآن جاهزون لمتابعة هذا المقال والبدء في استخدام الدوال في لغة SQL. فهم الدوال في لغة SQL الدوال functions هي عدة تعابير مجمعة ولها اسم محدد، قد تأخذ قيمة واحدة أوعدة قيم تمرر كوسطاء بين قوسين بعد اسم الدالة، وتجري عمليات حسابية أو تحويلات عليها، ثم تعيد قيمة جديدة كنتيجة للدالة، حيث تشبه دوال SQL الدوال الرياضية، فمثلًا تأخذ الدالة log(x) قيمة x وتعيد قيمة لوغاريتم هذه القيمة x. يمكننا استرجاع المعلومات من قاعدة بيانات علاقية دون تحويلها من خلال استخدام استعلام SELECT، حيث نطلب من قاعدة البيانات إعادة قيم للأعمدة الفردية التي نريدها من خلال تحديد أسماء الأعمدة في الاستعلام، فمثلًا إذا أردنا استرجاع جميع عناوين الكتب مع أسعارها وترتيبها من الأكثر إلى الأقل تكلفة، فيمكن تنفيذ التعليمة التالية: mysql> SELECT title, price, introduction_date FROM inventory ORDER BY price DESC; وسيظهر الخرج التالي: الخرج +------------------------------------+-------+-------------------+ | title | price | introduction_date | +------------------------------------+-------+-------------------+ | Pride and Prejudice | 42.13 | 2022-10-04 | | The Adventures of Huckleberry Finn | 23.15 | 2022-10-01 | | The Time Machine | 21.99 | 2022-09-23 | | The Picture of Dorian Gray | 20.83 | 2022-10-01 | | Frankenstein | 17.43 | 2022-07-23 | +------------------------------------+-------+-------------------+ 5 rows in set (0.000 sec) تكون title و price و introduction_date في التعليمة السابقة أسماء الأعمدة، وتعطي قاعدة البيانات في الخرج الناتج قيمًا غير مُعدَّلة مُسترجَعة من تلك الأعمدة لكل كتاب وهي عنوان الكتاب، والسعر ، وتاريخ إدخال الكتاب إلى المكتبة. قد نرغب في استرجاع القيم من قاعدة البيانات بعد إجراء بعض المعالجة، فقد نكون مهتمين بأسعار الكتب مع تقريبها إلى أقرب سعر بالدولار، أو كتابة عناوين الكتب بحروف كبيرة، أو عرض سنة تقديم الكتاب دون تضمين الشهر أو اليوم، لذا يجب أن نستخدم دوال مناسبة لذلك. أشهر الدوال في لغة SQL يمكن تصنيف دوال SQL إلى عدة مجموعات اعتمادًا على نوع البيانات التي نعمل عليها، ونوضح فيما يلي الدوال الأكثر استخدامًا في SQL. الدوال الرياضية هي الدوال التي تعمل على معالجة القيم العددية وتجري العمليات الحسابية عليها، مثل التقريب أو اللوغاريتمات أو الجذور التربيعية أو الرفع لأس دوال معالجة السلاسل النصية هي دوال تعمل على السلاسل والحقول النصية، وتجري تحويلات نصية مثل تحويل أحرف النص إلى حروف كبيرة، أو إزالة الفراغات من بدايات ونهايت السلاسل النصية Trimming، أو استبدال الكلمات في النص بكلمات أخرى. دوال التاريخ والوقت هي دوال تتعامل مع الحقول التي تحتوي على تواريخ، وتُجري هذه الدوال عمليات حسابية وتحويلات مثل إضافة عدد من الأيام إلى التاريخ المُحدَّد، أو أخذ سنة واحدة فقط من التاريخ الكامل. الدوال التجميعية Aggregate Functions هي حالة خاصة من الدوال الرياضية التي تعمل على قيم قادمة من صفوف متعددة مثل حساب متوسط السعر لجميع الصفوف. مثال على استدعاء دالة يوضّح المثال التالي الصيغة العامة لاستخدام دالة وهمية بالاسم EXAMPLE لتغيير نتائج قيم السعر price في قاعدة بيانات المكتبة باستخدام استعلام SELECT التالي: mysql> SELECT EXAMPLE(price) AS new_price FROM inventory; تأخذ الدالة EXAMPLE اسم العمود price كوسيط ، حيث يخبر هذا الجزء من الاستعلام قاعدة البيانات بتنفيذ الدالة EXAMPLE على قيم العمود price وإعادة نتائج هذه العملية، ويخبر جزء الاستعلام AS new_price قاعدة البيانات بإسناد اسم مؤقت new_price للقيم المحسوبة خلال فترة تنفيذ الاستعلام، وبذلك يمكننا تمييز نتائج الدالة في الخرج والإشارة إلى القيم الناتجة باستخدام تعلميتي WHERE و ORDER BY. سنستخدم في القسم التالي الدوال الرياضية لإجراء العمليات الحسابية شائعة الاستخدام. استخدام الدوال الرياضية تعمل الدوال الرياضية على القيم العددية مثل سعر الكتاب أو عدد الكتب الموجودة في المخزن ضمن قاعدة البيانات التجريبية، ويمكن استخدامها لإجراء العمليات الحسابية في قاعدة البيانات لمطابقة النتائج مع متطلباتنا. يُعَد التقريب من التطبيقات الأكثر استخدامًا للدوال الرياضية في لغة SQL، فلنفترض مثلًا أننا نريد استرجاع أسعار جميع الكتب، ولكن نريدها أن تكون مُقرَّبة لأقرب عدد صحيح، لذا نستخدم الدالة ROUND التي تطبّق عملية التقريب، ونجرّب تنفيذ التعليمة التالية: mysql> SELECT title, price, ROUND(price) AS rounded_price FROM inventory; سيظهر الخرج التالي: الخرج +------------------------------------+-------+---------------+ | title | price | rounded_price | +------------------------------------+-------+---------------+ | The Picture of Dorian Gray | 20.83 | 21 | | Pride and Prejudice | 42.13 | 42 | | The Time Machine | 21.99 | 22 | | Frankenstein | 17.43 | 17 | | The Adventures of Huckleberry Finn | 23.15 | 23 | +------------------------------------+-------+---------------+ 5 rows in set (0.000 sec) يختار الاستعلام السابق قيم الأعمدة title و price دون تعديل، بالإضافة إلى العمود rounded_price المؤقت الذي يحتوي على نتائج الدالة ROUND(price). تأخذ هذه الدالة وسيطًا واحدًا هو اسم العمود price في مثالنا، وتعيد القيم من هذا العمود في الجدول مع تقريبها إلى أقرب قيمة عددية صحيحة. يمكن لدالة التقريب أيضًا قبول وسيط إضافي يحدّد عدد المنازل العشرية التي يجب تطبيق التقريب عليها، ويمكن إضافة عمليات حسابية بدلًا من الاكتفاء باسم عمود واحد. لنجرّب مثلًا تشغيل الاستعلام التالي: mysql> SELECT title, price, ROUND(price * stock, 1) AS stock_price FROM inventory; وسيظهر الخرج التالي: الخرج +------------------------------------+-------+-------+-------------+ | title | stock | price | stock_price | +------------------------------------+-------+-------+-------------+ | The Picture of Dorian Gray | 4 | 20.83 | 83.3 | | Pride and Prejudice | 12 | 42.13 | 505.6 | | The Time Machine | 7 | 21.99 | 153.9 | | Frankenstein | 9 | 17.43 | 156.9 | | The Adventures of Huckleberry Finn | 14 | 23.15 | 324.1 | +------------------------------------+-------+-------+-------------+ 5 rows in set (0.000 sec) يؤدي تنفيذ الدالة ROUND(price * stock, 1) أولًا إلى ضرب سعر الكتاب الواحد بعدد الكتب الموجودة في المخزن، ثم تقريب السعر الناتج إلى أول منزلة عشرية، وستُعرَض النتيجة في العمود المؤقت stock_price. تتضمّن الدوال الرياضية الأخرى المُضمَّنة في MySQL الدوال المثلثية والجذور التربيعية والرفع لقوة واللوغاريتمات والدوال الأسية. ويمكن معرفة المزيد حول استخدام الدوال الرياضية في لغة SQL في مقال كيفية استخدام التعابير الرياضية والدوال التجميعية في لغة SQL. استخدام دوال معالجة السلاسل النصية تمكّننا دوال معالجة السلاسل النصية في لغة SQL من تعديل القيم المُخزَّنة في الأعمدة التي تحتوي على نصوص عند معالجة استعلام SQL، ويمكن استخدامها لتحويل حالة الحروف، أو دمج بيانات من أعمدة متعددة، أو إجراء عمليات البحث والاستبدال. سنستخدم مثلًا دوال السلاسل النصية لاسترجاع جميع عناوين الكتب المُحوَّلة إلى حروف صغيرة، وننفّذ التعليمة التالية: mysql> SELECT LOWER(title) AS title_lowercase FROM inventory; وسيظهر الخرج التالي: الخرج +------------------------------------+ | title_lowercase | +------------------------------------+ | the picture of dorian gray | | pride and prejudice | | the time machine | | frankenstein | | the adventures of huckleberry finn | +------------------------------------+ 5 rows in set (0.001 sec) تأخذ دالة SQL التي هي LOWER وسيطًا واحدًا وتحوّل محتوياته إلى حروف صغيرة، وستُعرَض البيانات الناتجة في العمود المؤقت title_lowercase باستخدام تعليمة الاسم البديل للعمود AS title_lowercase. لنسترجع الآن جميع أسماء المؤلفين مع تحويلها إلى حروف كبيرة، لذا نجرّب تشغيل استعلام SQL التالي: mysql> SELECT UPPER(author) AS author_uppercase FROM inventory; وسيظهر الخرج التالي: الخرج +----------------------+ | author_uppercase | +----------------------+ | OSCAR WILDE | | JANE AUSTEN | | HERBERT GEORGE WELLS | | MARY SHELLEY | | MARK TWAIN | +----------------------+ 5 rows in set (0.000 sec) استخدمنا الدالة UPPER بدلًا من الدالة LOWER، حيث تعمل الدالة UPPER بطريقة مشابهة ولكنها تحوّل النص إلى حروف كبيرة، ويمكن استخدام كلتا الدالتين إذا أردنا ضمان تناسق حالة الحروف عند استرجاع البيانات. تأخذ الدالة CONCAT وسطاء متعددة تحتوي على قيم نصية وتضعها مع بعضها بعضًا، لذا سنجرب استرجاع مؤلفي الكتب وعناوين الكتب ودمجها في عمود واحد من خلال تنفيذ التعليمة التالية: mysql> SELECT CONCAT(author, ': ', title) AS full_title FROM inventory; وسيظهر الخرج التالي: الخرج +------------------------------------------------+ | full_title | +------------------------------------------------+ | Oscar Wilde: The Picture of Dorian Gray | | Jane Austen: Pride and Prejudice | | Herbert George Wells: The Time Machine | | Mary Shelley: Frankenstein | | Mark Twain: The Adventures of Huckleberry Finn | +------------------------------------------------+ 5 rows in set (0.001 sec) دمجت الدالة CONCAT السلاسل النصية المتعددة مع بعضها البعض، وقد نفّذناها باستخدام ثلاثة وسطاء: الوسيط الأول author يشير إلى العمود author الذي يحتوي على أسماء المؤلفين، والوسيط الثاني قيمة سلسلة نصية عشوائية للفصل بين المؤلفين وعناوين الكتب بنقطتين، والوسيط الأخير title يشير إلى العمود الذي يحتوي عناوين الكتب. تكون نتيجة هذا الاستعلام هي المؤلفين والعناوين في عمود مؤقت واحد بالاسم full_title. تتضمّن دوال السلاسل النصية الأخرى في MySQL دوال البحث عن السلاسل النصية واستبدالها، واسترجاع السلاسل النصية الفرعية، وإضافة حاشية Padding إلى السلاسل النصية وإزالة الفراغات Trimming من بداية ونهاية السلاسل النصية، وتطبيق التعابير النمطية Regular Expressions وغيرها من الدوال. ويمكنكم مطالعة المزيد حول استخدام دوال SQL لضم قيم متعددة في مقال كيفية معالجة البيانات باستخدام دوال CAST وتعابير الضم في SQL، ويمكنك أيضًا الرجوع إلى دليل دوال ومعاملات السلاسل النصية في توثيق MySQL. استخدام دوال التاريخ والوقت تمكّننا دوال التاريخ والوقت في لغة SQL من معالجة القيم المُخزَّنة في الأعمدة التي تحتوي على التواريخ والعلامات الزمنية عند معالجة استعلام SQL، ويمكن استخدامها لاستخراج أجزاء من معلومات التاريخ، أو إجراء عمليات حسابية على التواريخ، أو تنسيقها بطريقة محددة. لنفترض أننا نحتاج لتقسيم أو فصل تاريخ تقديم الكتاب إلى السنة والشهر واليوم بدلًا من وجود عمود تاريخ واحد في الخرج. لنجرّب تنفيذ التعليمة التالية: mysql> SELECT introduction_date, YEAR(introduction_date) as year, MONTH(introduction_date) as month, DAY(introduction_date) as day FROM inventory; سيظهر الخرج التالي: الخرج +-------------------+------+-------+------+ | introduction_date | year | month | day | +-------------------+------+-------+------+ | 2022-10-01 | 2022 | 10 | 1 | | 2022-10-04 | 2022 | 10 | 4 | | 2022-09-23 | 2022 | 9 | 23 | | 2022-07-23 | 2022 | 7 | 23 | | 2022-10-01 | 2022 | 10 | 1 | +-------------------+------+-------+------+ 5 rows in set (0.000 sec) استخدمنا في تعليمة SQL السابقة ثلاث دوال فردية هي: YEAR و MONTH و DAY، وتأخذ كل دالة اسم العمود بحيث تُخزَّن التواريخ كوسطاء، وتستخرج جزءًا واحدًا فقط من التاريخ الكامل: السنة أو الشهر أو اليوم على التوالي، وبالتالي يمكننا الوصول إلى أجزاء التاريخ الفردية ضمن استعلامات SQL باستخدام هذه الدوال. تسمح الدالة DATEDIFF باسترجاع عدد الأيام بين تاريخين، فلنحاول التحقق من عدد الأيام المنقضية بين تاريخ تقديم كل كتاب والتاريخ الحالي، لذا نشغّل الاستعلام التالي: mysql> SELECT introduction_date, DATEDIFF(introduction_date, CURRENT_DATE()) AS days_since FROM inventory; وسيظهر الخرج التالي: الخرج +-------------------+------------+ | introduction_date | days_since | +-------------------+------------+ | 2022-10-01 | -30 | | 2022-10-04 | -27 | | 2022-09-23 | -38 | | 2022-07-23 | -100 | | 2022-10-01 | -30 | +-------------------+------------+ 5 rows in set (0.000 sec) تأخذ الدالة DATEDIFF وسيطين هما: تاريخ البدء وتاريخ الانتهاء، وتحسب هذه الدالة عدد الأيام التي تفصل بينهما، وقد تكون النتيجة عددًا سالبًا إذا جاء تاريخ الانتهاء أولًا. الوسيط الأول في المثال السابق هو اسم العمود introduction_date الذي يحتوي على التواريخ في الجدول inventory، والوسيط الثاني هو الدالة CURRENT_DATE التي تمثل تاريخ النظام الحالي، ويؤدي تنفيذ هذا الاستعلام إلى استرجاع عدد الأيام بين هذين التاريخين، ويضع النتائج في العمود المؤقت days_since. ملاحظة: لا تُعَد الدالة DATEDIFF جزءًا من مجموعة دوال SQL المعيارية الرسمية، حيث تدعم العديد من قواعد البيانات هذه الدالة، ولكن قد تختلف الصيغة بين محرّكات قواعد البيانات المختلفة، وقد اتبعنا في هذا المثال صيغة MySQL الأصيلة. تتضمن دوال معالجة التاريخ الأخرى المُضمَّنة في MySQL جمع وطرح فترات التواريخ والوقت، أو تنسيق التواريخ بتنسيقات لغات مختلفة، أو استرجاع اسم اليوم والشهر أو إنشاء قيم تواريخ جديدة. ويمكن معرفة المزيد حول التعامل مع التواريخ في لغة SQL في مقال كيفية التعامل مع التواريخ والأوقات في SQL، ويمكن أيضًا الاطلاع على دوال التاريخ والوقت في توثيق MySQL. استخدام الدوال التجميعية Aggregate Functions استخدمنا في الأمثلة السابقة دوال SQL لتطبيق التحويلات أو العمليات الحسابية على قيم الأعمدة الفردية في صفٍ واحد، والذي يمثّل كتابًا في مكتبة، وتوفر لغة SQL طريقة لإجراء العمليات الحسابية على صفوف متعددة لمساعدتنا في العثور على معلومات مُجمَّعة حول مجموعة البيانات بأكملها. تتضمّن الدوال التجميعية الأساسية في لغة SQL الدوال التالية: AVG: لإيجاد متوسط القيم التي نجري العمليات الحسابية عليها COUNT: لإيجاد عدد القيم التي نجري العمليات الحسابية عليها MAX: لإيجاد القيمة الكبرى من مجموعة قيم MIN: لإيجاد القيمة الصغرى من مجموعة قيم SUM: لإيجاد مجموع جميع القيم يمكننا دمج دوال تجميعية متعددة في استعلام SELECT، فلنفترض أننا نريد التحقق من عدد الكتب المدرجَة في المكتبة، والسعر الأكبر لأي كتاب متاح، ومتوسط الأسعار لكافة الكتب، لذا ننفّذ التعليمة التالية: mysql> SELECT COUNT(title) AS count, MAX(price) AS max_price, AVG(price) AS avg_price FROM inventory; تعيد هذه التعليمة الخرج التالي: الخرج +-------+-----------+-----------+ | count | max_price | avg_price | +-------+-----------+-----------+ | 5 | 42.13 | 25.106000 | +-------+-----------+-----------+ 1 row in set (0.001 sec) يستخدم الاستعلام السابق ثلاث دوال تجميعية في الوقت نفسه، حيث تحسب الدالة COUNT عدد الصفوف التي يبحث عنها الاستعلام، ومرّرنا اسم العمود title كوسيط لهذه الدالة، ولكن يمكنك استخدام أي اسم عمود آخر كوسيط لها لأن عدد الصفوف هو نفسه لكل الأعمدة. تحسب الدالة MAX القيمة الكبرى للعمود price، ويكون اسم العمود مهمًا في هذه الحالة، بسبب إجراء الحسابات على قيم هذا العمود. الدالة الأخيرة هي الدالة AVG التي تحسب المتوسط الحسابي لجميع الأسعار من العمود price. يؤدي استخدام الدوال التجميعية بهذه الطريقة إلى إعادة قاعدة البيانات لصف واحد يحتوي على أعمدة مؤقتة تمثّل قيم العمليات الحسابية المُجمَّعة، حيث تستخدم العمليات الحسابية الصفوف المصدر داخليًا ولكن لا يعيدها الاستعلام. يمكن باستخدام لغة SQL أيضًا تقسيم الصفوف في الجدول إلى مجموعات، ثم حساب القيم المُجمَّعة لكل مجموعة على حِدة، فمثلًا يمكننا حساب متوسط أسعار الكتب لمؤلفين مختلفين لمعرفة المؤلف الذي ينشر العناوين ذات السعر الأعلى، حيث يمكنك معرفة المزيد حول تجميع الصفوف لمثل هذه العمليات الحسابية في مقال كيفية استخدام تعليمتي GROUP BY و ORDER BY في لغة SQL، ويمكن مطالعة مزيد من التفاصيل حول استخدام الدوال التجميعية في مقال كيفية استخدام التعابير الرياضية والدوال التجميعية في SQL. الخلاصة تعلّمنا في هذا المقال ما هي دوال SQL وكيفية استخدامها لمعالجة الأعداد والسلاسل النصية والتواريخ من خلال الأمثلة العملية، ويمكن بالطبع استخدام الدوال لمعالجة وتحليل البيانات في محرّك قاعدة البيانات بطرق عديدة أخرى، ولكننا غطينا في هذا المقال الدوال الأساسية فقط. وإذا أردنا التعامل مع البيانات وتحليلها بطرق فعّالة، فيمكننا دمج الدوال مع الاستعلامات الشرطية، ومع تعليمات التجميع في لغة SQL. ترجمة -وبتصرف- للمقال How To Use Functions in SQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: استخدام عمليات الدمج Union في لغة SQL أنواع البيانات في SQL التجميع والترتيب في SQL البحث والتنقيب والترشيح في SQL دوال التعامل مع البيانات في SQL
-
سنشرح في هذا المقال آلية معالجة العقد ضمن شجرة المشاهد والترتيب الذي يتبعه محرك الألعاب جودو Godot للتعامل معها، كما سنوضح ما هي مسارات العقد وكيفية التنقل بينها، سيساعدنا ذلك على فهم طريقة تنظيم لعبتنا، والتحكم بها بفعالية أكبر. ترتيب معالجة العقد في شجرة المشاهد يتضمن محرك ألعاب جودو مفهوم يسمى شجرة المشاهد Scene Tree تتكون هذه الشجرة من عدة عقد Nodes، تمثل كل عقدة جزءًا من مشهد اللعبة، ويُذكَر مصطلح ترتيب الشجرة Tree Order في توثيق جودو، ولكنه غير واضح بالنسبة للمبتدئين، حيث يكون هذا الترتيب من أعلى الشجرة لأسفلها بدءًا من الجذر نزولًا إلى كل فرع بدوره، أي يبدأ الترتيب من العقد الرئيسية ثم يتنقل عبر الفروع وصولًا للعقد الفرعية، وهذا الترتيب مهم لأن كل عقدة تؤثر في العقد التي تحتها، لاحظ هذا الترتيب في الشكل التالي: سنرفق هذا الكود بكل عقدة: extends Node func _init(): # ملاحظة: العقدة ليس لها اسم بعد هنا print("TestRoot init") func _enter_tree(): print(name + " enter tree") func _ready(): print(name + " ready") # يضمن ما يلي أننا نطبع مرة واحدة فقط في process() var test = true func _process(delta): if test: print(name + " process") test = false يوضح الكود أعلاه كيفية تفاعل العقدة مع الأحداث المختلفة في شجرة المشاهد، ولنوضح ما يمثله استدعاء كل دالة من الدوال الواردة فيه قبل أن نتحدث عن النتائج: تستدعى الدالة _init() عند إنشاء العقدة أو الكائن لأول مرة، وتكون العقدة حينها موجودة في ذاكرة الحاسوب ولم تُضف لشجرة المشاهد تستدعى الدالة _enter_tree() عند إضافة العقدة للشجرة لأول مرة، ويمكن أن يحدث ذلك عند إنشاء نسخة من العقدة أو عند إنشاء عقدة ابن من عقدة ما باستخدام التابع add_child() مثلًا تستدعى الدالة _ready() عند اكتمال إضافة العقدة وأبنائها بنجاح لشجرة المشاهد وجاهزيتها للعمل تستدعى الدالة _process() بشكل دوري في كل إطار -60 مرة في الثانية عادةً- وذلك لكل عقدة في الشجرة ويمكن استخدامها للتعامل مع التحديثات المتكررة إذا شغّلنا الكود على عقدة واحدة بمفردها، فسيكون ترتيب استدعاء الدوال كالمتوقع وفق ما يلي: TestRoot init TestRoot enter tree TestRoot ready TestRoot process لكن إذا أضفنا عقد أبناء، فسيصبح الأمر أكثر تعقيدًا وقد يحتاج إلى بعض التوضيح: TestRoot init TestChild1 init TestChild3 init TestChild2 init TestRoot enter tree TestChild1 enter tree TestChild3 enter tree TestChild2 enter tree TestChild3 ready TestChild1 ready TestChild2 ready TestRoot ready TestRoot process TestChild1 process TestChild3 process TestChild2 process طبعت جميع هذه العقد رسائلها بترتيب الشجرة من الأعلى إلى الأسفل باستثناء الشيفرة البرمجية الخاصة بالتابع _ready() والذي يُستدعَى عندما تكون العقدة جاهزة، أي عندما تدخل العقدة وأبناؤها شجرة المشاهد، فإذا كان للعقدة أبناء، فستُشغَّل استدعاءات التابع _ready الخاصة بالأبناء أولًا، وستتلقى العقدة الأب إشعار الجاهزية بعد ذلك كما يوضح توثيق جودو الرسمي. يقودنا ذلك إلى قاعدة أساسية مهمة يجب تذكرها عند إعداد بنية العقد وهي: يجب أن تدير العقد الأب أبناءها وليس العكس، ويجب أن تكون أيّ شيفرة برمجية للعقدة الأب قادرةً على الوصول الكامل إلى بيانات أبنائها، لذا يجب معالجة استدعاءات التابع _ready() بترتيب الشجرة العكسي. علينا تذكّر ذلك عند محاولة الوصول لعقد أخرى في التابع _ready()، فإذا كنا بحاجة للانتقال لأعلى الشجرة إلى عقدة أب أو عقدة جَد، فيجب تشغيل هذه الشيفرة البرمجية في العقدة الأب وليس في العقدة الابن. فهم مسارات العقد والتنقل في شجرة المشاهد تُستخدم مسارات العقد Node Paths في جودو لفهم كيفية التنقل بين العقد في شجرة المشاهد Scene Tree. وهذه المسارات أساسية لفهم كيفية الوصول للعقد المختلفة داخل الشجرة وتجنب مشكلة وجود مرجع عقدة غير صالح، والتي تظهر على هيئة رسالة خطأ كالتالي: Invalid get index ‘position’ (on base: ’null instance’). يُعَد الجزء الأخير من رسالة الخطأ null instance مصدر هذه المشكلة، وهو يسبب إرباكًا للمبتدئين في جودو، ويمكن تجنّب هذه المشكلة من خلال فهم مفهوم مسارات العقد. مسارات العقد تتكون شجرة المشهد من عقد ترتبط ببعضها البعض بعلاقات أب-ابن، ومسار العقد هو المسار المُتّخَذ للانتقال من عقدة إلى أخرى من خلال التحرّك عبر هذه الشجرة. لنأخذ مثلًا مشهد لاعب بسيط كما يلي: يوجد كود هذا المشهد في العقدة Player. إذا كان السكربت بحاجة إلى استدعاء الدالة play() مع العقدة AnimatedSprite، فسيحتاج إلى مرجع إلى تلك العقدة: get_node("AnimatedSprite").play() إن وسيط الدالة get_node() هو سلسلة نصية تمثّل المسار إلى العقدة المطلوبة، وتكون هذه السلسلة النصية في حالتنا هي ابن العقدة التي يوجد ضمنها الكود. إذا كان المسار المُقدّم لها غير صالح، فسنحصل على خطأ null instance وخطأ عدم العثور على العقدة Node not found أيضًا. يُعَد الحصول على مرجع عقدة باستخدام الدالة get_node() حالة شائعة لدرجة أن لغة GDScript لديها اختصار له حيث يمكنك كتابة $ للوصول إلى العقدة مباشرة بدلًا من استدعاء الدالة، على سبيل المثال للوصول إلى العقدة AnimatedSprite وتشغيل الدالة ()play عليها مباشرة نكتب: $AnimatedSprite.play() ملاحظة: تعيد الدالة get_node() مرجعًا Reference إلى العقدة المطلوبة. لنأخذ الآن مثالًا لشجرة مشهد أكثر تعقيدًا كما يلي: إذا احتاج الكود المرفق بالعقدة Main إلى الوصول إلى العقدة ScoreLabel، فيمكنه ذلك باستخدام هذا المسار: get_node("HUD/ScoreLabel").text = "0" # أو باستخدام الاختصار: $HUD/ScoreLabel.text = "0" ملاحظة: سيكمل محرر جودو المسارات تلقائيًا نيابةً عنا عند استخدام صيغة $، ويمكننا أيضًا النقر بزر الفأرة الأيمن على عقدة ما في تبويب المشهد Scene واختيار نسخ مسار العقدة Copy Node Path. إذا كانت العقدة التي نريد الوصول إليها موجودة في مكان أعلى من الشجرة، فيمكننا استخدام الدالة get_parent() أو ".." للإشارة إلى العقدة الأب، حيث يمكننا الحصول على العقدة Player من العقدة ScoreLabel في شجرة المثال السابق كما يلي: get_node("../../Player") يمثّل المسار "../../Player" الحصول على العقدة التي تقع في مستوى واحد أعلى HUD ثم العقدة التي تقع في مستوى أعلى وهي Main ثم الوصول إلى العقدة الابن لها وهي Player. ملاحظة: تعمل مسارات العقد مثل مسارات المجلدات في نظام التشغيل، حيث تشير الشرطة المائلة / إلى علاقة أب-ابن، وتعني .. مستوى واحد أعلى. المسارات النسبية Relative والمسارات المطلقة Absolute تستخدم جميع الأمثلة السابقة مسارات نسبية، لأنها تبدأ من العقدة الحالية وتتبع المسار إلى الوجهة، ولكن يمكن أن تكون مسارات العقد مطلقة أيضًا بحيث تبدأ من العقدة الجذر للمشهد، فمثلًا يكون المسار المطلق إلى عقدة اللاعب هو: get_node("/root/Main/Player") لا يعيد المسار /root والذي يمكن الوصول إليها أيضًا باستخدام get_tree().root العقدة الجذر لمشهدنا الحالي، ولكنه يعيد العقدة الجذر لنافذة العرض Viewport التي توجد دائمًا في شجرة المشهد SceneTree افتراضيًا. مشكلة في التعامل مع مسارات العقد في جودو تعمل الأمثلة السابقة بنجاح، ولكن توجد بعض الأشياء التي يجب أن نكون على دراية بها والتي قد تسبب مشكلات لاحقًا. لنفترض أن لدينا الحالة التالية: تحتوي العقدة Player على الخاصية health التي نريد عرضها في العقدة HealthBar في مكان ما في واجهة المستخدم الخاصة بنا، لذا يمكن كتابة شيء يشبه ما يلي في كود اللاعب: func take_damage(amount): health -= amount get_node("../Main/UI/HealthBar").text = str(health) قد يكون هذا السكربت جيدًا في البداية، ولكن يمكن أن يواجه خطأ بسهولة، إذ توجد مشكلتان رئيسيتان في هذا النوع من الترتيب وهما: لا يمكننا اختبار مشهد اللاعب بصورة مستقلة، فإذا شغلنا مشهد اللاعب بمفرده أو في مشهد اختبار دون واجهة مستخدم، فسيسبّب سطر الدالة get_node() في حدوث عطل لا يمكننا تغيير واجهة المستخدم الخاصة بنا، فإذا قررنا إعادة ترتيبها أو تصميمها، فلن يكون المسار صالحًا بعد الآن ويجب تغييره لذا علينا تجنب استخدام مسارات العقد التي تنتقل إلى الأعلى في شجرة المشهد. إذا أصدر اللاعب في المثال السابق إشارة عند تغير مستوى صحته health، فيمكن لواجهة المستخدم الاستماع إلى هذه الإشارة لتحديث نفسها، ثم يمكننا إعادة ترتيب العقد والفصل بينها دون الخوف من توقف اللعبة. الخاتمة نأمل أن يكون هذا المقال قد ساعدكم على تكوين فكرة واضحة حول استخدام مسارات العقد في جودو، والطريقة الصحيحة للتنقل بين العقد والاتصال بالعناصر التي تحتاجوها في شجرة المشاهد. ففهم مسارات العقد هو الأساس الذي يمكننا البناء عليه لتفادي العديد من الأخطاء الشائعة ورسائل الخطأ مثل null instance والإشارة لأي عقدة نحتاجها بالطريقة الصحيحة. ترجمة -وبتصرّف- للقسمين Understanding tree order و Understanding node paths من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إنشاء شخصيات ثلاثية الأبعاد في جودو Godot كتابة سكربتات GDScript وإرفاقها بالعقد في جودو استخدام الإشارات Signals في جودو Godot لغات البرمجة المتاحة في جودو Godot
-
تنشر العديد من قواعد البيانات المعلومات على جداول مختلفة بحسب معناها وسياقها، وبالتالي قد نحتاج إلى الإشارة إلى أكثر من جدول واحد عند استرجاع معلومات حول البيانات الموجودة في قاعدة البيانات، لذا توفّر لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- طرقًا متعددة لاسترجاع البيانات من جداول مختلفة مثل عمليات المجموعة Set Operations، وخاصةً معامل المجموعة UNION الذي تدعمه معظم أنظمة قواعد البيانات العلاقية Relational Database Systems والذي يأخذ نتائج استعلامين لأعمدة متطابقة ويدمجهما في استعلام واحد. سنشرح في هذا المقال طريقة استخدام المعامل UNION لاسترجاع ودمج البيانات من أكثر من جدول، ونتعلم كيفية دمجه مع عملية الترشيح Filtering لترتيب النتائج. مستلزمات العمل يجب أن يكون لديك حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- المستند إلى لغة SQL. اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم بصلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو، ومقال كيفية تثبيت توزيعة أوبنتو من لينكس بأبسط طريقة نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضّح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الفقرة التالية من المقال. معرفة أساسية بتنفيذ استعلامات SELECT لاختيار البيانات من قاعدة البيانات كما هو موضّح في مقال كيفية الاستعلام عن السجلات من الجداول في SQL ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدّمة في هذا المقال بنجاح مع معظم هذه الأنظمة وهي جزء من صيغة لغة SQL المعيارية، ولكن قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لنتمكّن من التدرب على استخدام عمليات UNION. وسنوضح في القسم التالي تفاصيل حول الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية، والتي سنستخدمها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل في هذا القسم بخادم MySQL وسننشئ قاعدة بيانات تجريبية لنتمكّن من اتباع الأمثلة الواردة في هذا المقال. إذا كانت قاعدة بيانات SQL الخاصة بنا تعمل على خادم بعيد، علينا الاتصال بالخادم مُستخدمين بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم bookstore: mysql> CREATE DATABASE bookstore; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات bookstore من خلال تنفيذ تعليمة USE التالية: mysql> USE bookstore; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، وسننشئ عدة جداول تجريبية ضمنها، حيث سنستخدم في هذا المقال مكتبة افتراضية تقدّم خدمة شراء الكتب وتأجيرها، وتُدار كل خدمة من هاتين الخدمتين على حدة، وبالتالي ستُخزَّن البيانات المتعلقة بشراء الكتب وتأجيرها في جداول منفصلة. ملاحظة: بسّطنا مخطط قاعدة البيانات لهذا المثال للتوضيح، إذ تكون هياكل الجداول أكثر تعقيدًا في الحياة الواقعية مع تضمين مفاتيح رئيسية Primary Keys ومفاتيح خارجية Foreign Keys ضمنها. ويمكن مطالعة مقال فهم قواعد البيانات العلاقية لمزيد من المعلومات حول كيفية تنظيم البيانات. يحتوي الجدول الأول book_purchases على بيانات حول الكتب المُشتراة والعملاء الذين أجروا عمليات الشراء، إذ سيحتوي على أربعة أعمدة هي: purchase_id: يحتوي هذا العمود على معرّف عملية الشراء ونمثّله بنوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي للجدول، حيث تصبح كل قيمة معرّفًا فريدًا للصف الخاص بها customer_name: يحتوي هذا العمود على اسم العميل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 30 محرفًا book_title: يحتوي هذا العمود على عنوان الكتاب الذي جرى شراؤه، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 200 محرف date: يستخدم هذا العمود نوع البيانات date، ويحتوي على تاريخ كل عملية شراء ننشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE book_purchases ( mysql> purchase_id int, mysql> customer_name varchar(30), mysql> book_title varchar(40), mysql> date date, mysql> PRIMARY KEY (purchase_id) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول الأول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) يحزّن الجدول الثاني book_leases معلومات حول الكتب المُستعارة، وتكون بنيته مشابهة للجدول السابق، ولكن نميّز عملية تأجير الكتاب بتاريخين مختلفين هما تاريخ التأجير ومدته، وبالتالي سيحتوي هذا الجدول على خمسة أعمدة هي: lease_id: يحتوي هذا العمود على معرّف عملية التأجير، ونمثّله بنوع البيانات int، ويكون هذا العمود هو المفتاح الرئيسي للجدول، حيث تصبح كل قيمة معرّفًا فريدًا للصف الخاص بها customer_name: يحتوي هذا العمود على اسم العميل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 30 محرفًا book_title: يحتوي هذا العمود على عنوان الكتاب المُستعار، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 200 محرف date_from: يستخدم نوع البيانات date، ويحتوي هذا العمود على تاريخ بدء عملية التأجير date_to: يستخدم نوع البيانات date، ويحتوي هذا العمود على تاريخ انتهاء عملية التأجير لننشئ الجدول الثاني باستخدام الأمر التالي: mysql> CREATE TABLE book_leases ( mysql> lease_id int, mysql> customer_name varchar(30), mysql> book_title varchar(40), mysql> date_from date, mysql> date_to date, mysql> PRIMARY KEY (lease_id) ); يؤكّد الخرج التالي إنشاء الجدول الثاني: الخرج Query OK, 0 rows affected (0.00 sec) لنحمّل الآن الجدول book_purchases ببعض البيانات التجريبية من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO book_purchases mysql> VALUES mysql> (1, 'sammy', 'The Picture of Dorian Gray', '2023-10-01'), mysql> (2, 'sammy', 'Pride and Prejudice', '2023-10-04'), mysql> (3, 'sammy', 'The Time Machine', '2023-09-23'), mysql> (4, 'bill', 'Frankenstein', '2023-07-23'), mysql> (5, 'bill', 'The Adventures of Huckleberry Finn', '2023-10-01'), mysql> (6, 'walt', 'The Picture of Dorian Gray', '2023-04-15'), mysql> (7, 'walt', 'Frankenstein', '2023-10-13'), mysql> (8, 'walt', 'Pride and Prejudice', '2023-10-19'); ستضيف عملية INSERT INTO السابقة 8 عمليات شراء مع قيمها المحدَّدة إلى جدول book_purchases، حيث يشير الخرج التالي إلى إضافة جميع الصفوف الثمانية: الخرج Query OK, 8 rows affected (0.00 sec) Records: 8 Duplicates: 0 Warnings: 0 ثم ندخِل بعض البيانات التجريبية في الجدول book_leases كما يلي: mysql> INSERT INTO book_leases mysql> VALUES mysql> (1, 'sammy', 'Frankenstein', '2023-09-14', '2023-11-14'), mysql> (2, 'sammy', 'Pride and Prejudice', '2023-10-01', '2023-12-31'), mysql> (3, 'sammy', 'The Adventures of Huckleberry Finn', '2023-10-01', '2023-12-01'), mysql> (4, 'bill', 'The Picture of Dorian Gray', '2023-09-03', '2023-09-18'), mysql> (5, 'bill', 'Crime and Punishment', '2023-09-27', '2023-12-05'), mysql> (6, 'kim', 'The Picture of Dorian Gray', '2023-10-01', '2023-11-15'), mysql> (7, 'kim', 'Pride and Prejudice', '2023-09-08', '2023-11-17'), mysql> (8, 'kim', 'The Time Machine', '2023-09-04', '2023-10-23'); وسيظهر الخرج التالي الذي يؤكّد إضافة هذه البيانات: الخرج Query OK, 8 rows affected (0.00 sec) Records: 8 Duplicates: 0 Warnings: 0 تتعلق عمليات التأجير والشراء بعملاء وعناوين كتب متماثلة، مما سيكون مفيدًا لتوضيح سلوك المعامل UNION، وبذلك سنكون جاهزين لمتابعة هذا المقال والبدء في استخدام عمليات UNION في لغة SQL. فهم صيغة المعامل UNION يطلب المعامل UNION في لغة SQL من قاعدة البيانات أخذ مجموعتي نتائج منفصلتين ومسترجعتين من استعلامات SELECT فردية ودمجهما في مجموعة نتائج واحدة تحتوي على صفوف مُعادة من كلا الاستعلامين. ملاحظة: يمكن أن تكون استعلامات SELECT المستخدمة مع العملية UNION معقدة وتتطلب استخدام تعليمات JOIN أو دوال تجميعية Aggregations أو استعلامات الفرعية Subqueries. إذ يمكننا استخدام عمليات UNION لدمج نتائج الاستعلامات مهما كانت معقدة، ولكن للسهولة سنستخدم في أمثلتنا التالية استعلامات SELECT بسيطة للتركيز على كيفية تصرف المعامل UNION. يوضّح المثال التالي الصيغة العامة لتعليمة SQL التي تتضمن المعامل UNION: mysql> SELECT column1, column2 FROM table1 mysql> UNION mysql> SELECT column1, column2 FROM table2; تبدأ تعليمة SQL السابقة بتعليمة SELECT التي تعيد عمودين من الجدول table1، ونتبعها بالمعامل UNION وتعليمة SELECT ثانية، ويعيد استعلام SELECT الثاني أيضًا عمودين من الجدول table2. تخبر الكلمة المفتاحية UNION قاعدة البيانات بأخذ الاستعلامات السابقة واللاحقة، وتنفيذها تنفيذًا منفصلًا، ثم ضم مجموعات النتائج الخاصة بها ضمن مجموعة واحدة. يُعَد جزء الشيفرة البرمجية السابق بأكمله -بما في ذلك استعلامات SELECT والكلمة المفتاحية UNION بينهما- تعليمة SQL واحدة، لذلك لا ينتهي استعلام SELECT الأول بفاصلة منقوطة، والتي تظهر بعد اكتمال التعليمة فقط. لنفترض مثلًا أننا نريد إدخال جميع العملاء الذين اشتروا كتابًا أو استأجروه، حيث يحتفظ الجدول book_purchases بسجلات عمليات الشراء، بينما يخزّن الجدول book_leases عمليات تأجير الكتب، لذا لنشغّل الاستعلام التالي: mysql> SELECT customer_name FROM book_purchases mysql> UNION mysql> SELECT customer_name FROM book_leases; وتكون مجموعة نتائج هذا الاستعلام كما يلي: الخرج +---------------+ | customer_name | +---------------+ | sammy | | bill | | walt | | kim | +---------------+ 4 rows in set (0.000 sec) يشير هذا الخرج إلى أن Sammy و Bill و Walt و Kim اشتروا كتبًا أو استأجروها. دعونا نجرّب تنفيذ تعليمتي SELECT تنفيذًا منفصلًا، مرةً لعمليات الشراء ومرةً لعمليات التأجير لفهم كيفية إنشاء مجموعة هذه النتائج. لنشغّل أولًا الاستعلام التالي لإعادة العملاء الذين اشتروا كتبًا فقط: mysql> SELECT customer_name FROM book_purchases; وسيكون الخرج كما يلي: الخرج +---------------+ | customer_name | +---------------+ | sammy | | sammy | | sammy | | bill | | bill | | walt | | walt | | walt | +---------------+ 8 rows in set (0.000 sec) اشترى Sammy و Bill و Walt كتبًا، لكن Kim لم يفعل ذلك. ثم لنشغّل الاستعلام التالي لإعادة العملاء الذين استأجروا كتبًا فقط: mysql> SELECT customer_name FROM book_leases; وسيكون الخرج كما يلي: الخرج +---------------+ | customer_name | +---------------+ | sammy | | sammy | | sammy | | bill | | bill | | kim | | kim | | kim | +---------------+ 8 rows in set (0.000 sec) يشير الجدول book_leases إلى أن Sammy و Bill و Kim يستعيرون كتبًا، ولكن Walt لا يستعير كتبًا أبدًا. إذا جمعنا بين الإجابتين، فيمكننا الحصول على البيانات لكل من عمليات الشراء والتأجير. الفرق المهم بين استخدام عملية UNION وتنفيذ استعلامين تنفيذًا منفصلًا هو أن عملية UNION تزيل القيم المكررة، حيث تُدمَج النتائج دون تكرار أسماء العملاء في النتيجة. يجب أن يعيد كلا الاستعلامين النتائج بالتنسيق نفسه لاستخدام عملية UNION لدمج نتائج استعلامين منفصلين بطريقة صحيحة، إذ ستؤدي التعارضات إلى حدوث أخطاء في محرّك قاعدة البيانات أو إلى ظهور نتائج لا تتطابق مع الهدف من الاستعلام كما سنوضّح في المثالين التاليين. تنفيذ عملية UNION مع عدد غير متطابق من الأعمدة لنجرّب تنفيذ عملية UNION مع تعليمة SELECT تعيد عمودًا واحدًا وتعليمة SELECT أخرى تعيد عمودين: mysql> SELECT purchase_id, customer_name FROM book_purchases mysql> UNION mysql> SELECT customer_name FROM book_leases; سيستجيب خادم قاعدة البيانات بالخطأ التالي: الخرج The used SELECT statements have a different number of columns لا يمكن إجراء عمليات UNION على مجموعات النتائج التي لها أعداد أعمدة مختلفة. تنفيذ عملية UNION مع ترتيب غير متطابق للأعمدة لنجرّب الآن تنفيذ عملية UNION مع تعلميتي SELECT تعيدان القيم نفسها ولكن بترتيب مختلف كما يلي: mysql> SELECT customer_name, book_title FROM book_purchases mysql> UNION mysql> SELECT book_title, customer_name FROM book_leases; لن يعيد خادم قاعدة البيانات خطأ، ولكن لن تكون مجموعة النتائج صحيحة كما يلي: الخرج +------------------------------------+------------------------------------+ | customer_name | book_title | +------------------------------------+------------------------------------+ | sammy | The Picture of Dorian Gray | | sammy | Pride and Prejudice | | sammy | The Time Machine | | bill | Frankenstein | | bill | The Adventures of Huckleberry Finn | | walt | The Picture of Dorian Gray | | walt | Frankenstein | | walt | Pride and Prejudice | | Frankenstein | sammy | | Pride and Prejudice | sammy | | The Adventures of Huckleberry Finn | sammy | | The Picture of Dorian Gray | bill | | Crime and Punishment | bill | | The Picture of Dorian Gray | kim | | Pride and Prejudice | kim | | The Time Machine | kim | +------------------------------------+------------------------------------+ 16 rows in set (0.000 sec) تدمج عملية UNION في هذا المثال العمود الأول من الاستعلام الأول مع العمود الأول من الاستعلام الثاني وتفعل الشيء نفسه بالنسبة للعمود الثاني، وبالتالي ستخلط أسماء العملاء وعناوين الكتب مع بعضها بعضًا. استخدام الشروط وترتيب النتائج باستخدام UNION دمجنا في المثال السابق مجموعات النتائج التي تمثل جميع الصفوف في جدولين متطابقين، ولكن ستحتاج في كثير من الأحيان إلى ترشيح الصفوف وفق شروط محددة قبل دمج النتائج، حيث يمكن لتعليمات SELECT المدموجة باستخدام المعامل UNION استخدام تعليمة WHERE لتطبيق ذلك. لنفترض مثلًا أننا نريد معرفة الكتب التي يقرأها Sammy من خلال شرائها أو استئجارها، لذا نشغّل الاستعلام التالي: mysql> SELECT book_title FROM book_purchases mysql> WHERE customer_name = 'Sammy' mysql> UNION mysql> SELECT book_title FROM book_leases mysql> WHERE customer_name = 'Sammy'; يتضمن كلا الاستعلامين تعليمة WHERE، مما يؤدي إلى ترشيح الصفوف من الجدولين المنفصلين لتضمين عمليات الشراء والتأجير التي أجراها Sammy فقط، وستكون نتائج هذا الاستعلام كما يلي: الخرج +------------------------------------+ | book_title | +------------------------------------+ | The Picture of Dorian Gray | | Pride and Prejudice | | The Time Machine | | Frankenstein | | The Adventures of Huckleberry Finn | +------------------------------------+ 5 rows in set (0.000 sec) تضمن عملية UNION عدم وجود تكرارات في قائمة النتائج. يمكننا استخدام تعليمات WHERE أيضًا لتحديد الصفوف المُعادة في استعلامي SELECT أو أحدهما فقط، ويمكن أن تشير تعليمة WHERE إلى أعمدة وشروط مختلفة في كل من الاستعلامين. إضافةً لذلك، لا تتبع النتائج التي تعيدها عملية UNION ترتيبًا محدَّدًا، ولكن يمكننا تغيير ذلك من خلال استخدام تعليمة ORDER BY، حيث ينفَّذ الترتيب على النتائج النهائية المدموجة وليس على الاستعلامات الفردية. يمكننا فرز عناوين الكتب أبجديًا بعد استرجاع قائمة بجميع الكتب التي اشتراها أو استأجرها Sammy من خلال تنفيذ الاستعلام التالي: mysql> SELECT book_title FROM book_purchases mysql> WHERE customer_name = 'Sammy' mysql> UNION mysql> SELECT book_title FROM book_leases mysql> WHERE customer_name = 'Sammy' mysql> ORDER BY book_title; وسيكون الخرج كما يلي: الخرج +------------------------------------+ | book_title | +------------------------------------+ | Frankenstein | | Pride and Prejudice | | The Adventures of Huckleberry Finn | | The Picture of Dorian Gray | | The Time Machine | +------------------------------------+ 5 rows in set (0.001 sec) نلاحظ أن إعادة النتائج بالترتيب يعتمد على العمود book_title الذي يحتوي على النتائج الناتجة عن دمج استعلامَي SELECT. استخدام العملية UNION ALL للاحتفاظ بالنسخ المكررة يزيل المعامل UNION تلقائيًا الصفوف المكررة من النتائج كما أظهرت الأمثلة السابقة، ولكن قد لا يكون هذا السلوك هو ما نريد تحقيقه باستخدام الاستعلام، فمثلًا لنفترض أننا نريد معرفة الكتب المُشتراة أو المستأجرة في 1 من شهر 11 من عام 2023، حيث يمكننا استرجاع هذه العناوين من خلال اتباع المثال التالي المشابه لما سبق: mysql> SELECT book_title FROM book_purchases mysql> WHERE date = '2022-10-01' mysql> UNION mysql> SELECT book_title FROM book_leases mysql> WHERE date_from = '2022-10-01' mysql> ORDER BY book_title; وسنحصل على النتائج التالية: الخرج +------------------------------------+ | book_title | +------------------------------------+ | Pride and Prejudice | | The Adventures of Huckleberry Finn | | The Picture of Dorian Gray | +------------------------------------+ 3 rows in set (0.001 sec) عناوين الكتب المُعادة هنا صحيحة، ولكن لن تخبرنا النتائج فيما إذا كانت هذه الكتب مشتراة فقط أم مستأجرة فقط أم كليهما، حيث ستظهر عناوين الكتب في الجدولين book_purchases و book_leases في الحالات التي جرى فيها شراء بعض الكتب واستئجارها، ولكن ستُفقَد هذه المعلومات في النتيجة، لأن المعامل UNION يزيل الصفوف المكررة. تمتلك لغة SQL طريقة لتغيير هذا السلوك والاحتفاظ بالصفوف المكررة، حيث يمكننا استخدام المعامل UNION ALL لدمج النتائج من استعلامين دون إزالة الصفوف المكررة، ويعمل المعامل UNION ALL بطريقة مشابهة للمعامل UNION، ولكن إذا وُجِدت تكرارات متعددة للقيم نفسها، فستكون جميعها موجودة في النتيجة. لنشغّل الاستعلام السابق نفسه مع وضع UNION ALL مكان UNION كما يلي: mysql> SELECT book_title FROM book_purchases mysql> WHERE date = '2022-10-01' mysql> UNION ALL mysql> SELECT book_title FROM book_leases mysql> WHERE date_from = '2022-10-01' mysql> ORDER BY book_title; ستكون القائمة الناتجة أطول هذه المرة كما يلي: الخرج +------------------------------------+ | book_title | +------------------------------------+ | Pride and Prejudice | | The Adventures of Huckleberry Finn | | The Adventures of Huckleberry Finn | | The Picture of Dorian Gray | | The Picture of Dorian Gray | +------------------------------------+ 5 rows in set (0.000 sec) يظهَر الكتابان The Adventures of Huckleberry Finn و The Picture of Dorian Gray مرتين في مجموعة النتائج، وهذا يعني أنهما ظهرا في الجدولين book_purchases و book_leases، وبالتالي يمكننا افتراض أنه جرى تأجيرهما وشراؤهما في ذلك اليوم. يمكن الاختيار بين المعاملين UNION و UNION ALL والتبديل بينهما اعتمادًا على ما إذا أردنا إزالة التكرارات أو الاحتفاظ بها. ملاحظة: تنفيذ المعامل UNION ALL أسرع من تنفيذ المعامل UNION، إذ لا تحتاج قاعدة البيانات إلى فحص مجموعة النتائج للعثور على التكرارات. فإذا أردنا دمج نتائج استعلامات SELECT التي نعرف أنها لن تحتوي على أي صفوف مكررة، فيمكن أن يحقق استخدام المعامل UNION ALL أداءً أفضل مع مجموعات البيانات الأكبر حجمًا. الخلاصة تعرّفنا في هذا المقال على كيفية استرجاع البيانات من جداول متعددة باستخدام عمليات UNION و UNION ALL، واستخدمنا أيضًا تعليمات WHERE لترشيح النتائج وتعليمات ORDER BY لترتيبها، وتعلّمنا الأخطاء المحتمَلة والسلوكيات غير المتوقعة إذا أنتجت تعليمات SELECT تنسيقات بيانات مختلفة. يجب أن تعمل الأوامر الواردة في هذا المقال على معظم قواعد البيانات العلاقية، ولكن يجب أن نتذكر أن كل قاعدة بيانات SQL تستخدم تقديمها الفريد للغة، لذا ننصح بالاطلاع على مقال مقارنة بين أنظمة إدارة قواعد البيانات العلاقية: SQLite مع MySQL مع PostgreSQL لمزيد من المعلومات، كما ننصح بالرجوع إلى توثيق RDBMS الرسمي للحصول على شرح مفصّل لكل أمر ومجموعة خياراته الكاملة. ترجمة -وبتصرف- للمقال How To Use Unions in SQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: استخدام الاستعلامات المتداخلة Nested Queries في SQL الدمج بين الجداول في SQL أنواع قواعد البيانات وأهم مميزاتها واستخداماتها معالجة الأخطاء والتعديل على قواعد البيانات في SQL التعابير الجدولية الشائعة Common Table Expressions في SQL
-
نوضح في هذا المقال بعض المشكلات الشائعة التي قد يواجهها المطورون أثناء تدريب أو استخدام نماذج مكتبة المحولات Transformers، ونشرح كيفية إيجاد حلول فعالة لها. مشكلة تشغيل المكتبة في بيئات محمية بجدار حماية قد تكون بعض الخوادم أو الأجهزة التي تحتوي على وحدات معالجة الرسوميات GPU وتعمل في بيئات سحابية أو ضمن شبكات داخلية محمية بجدار حماية Firewall لا تسمح لها بالاتصال بالإنترنت، مما يؤدي إلى حدوث خطأ في الاتصال، عندها إذا حاول السكربت تنزيل أوزان النموذج أو مجموعات البيانات من الإنترنت عبر مكتبة Transformers، فسوف تتوقف عملية التنزيل وتظهر رسالة خطأ مشابهة للرسالة التالية: ValueError: Connection error, and we cannot find the requested files in the cached path. Please try again or make sure your Internet connection is on. لحل هذه المشكلة، نحتاج لتشغيل مكتبة المحولات Transformers في وضع عدم الاتصال بالإنترنت لتجنب حدوث هذا الخطأ. نفاد ذاكرة CUDA هو خطأ شائع يحدث عندما نحاول تشغيل نموذج تعلم عميق كبير على وحدة معالجة الرسوميات GPU لكن الذاكرة المتاحة لا تكفي لتحميل النموذج أو البيانات المطلوبة. فتدريب النماذج الكبيرة التي تحتوي على ملايين المعاملات أمر صعب بدون استخدام العتاد المناسب، وفي حال نفاد ذاكرة وحدة معالجة الرسوميات GPU سنحصل على رسالة خطأ كالتالي: CUDA out of memory. Tried to allocate 256.00 MiB (GPU 0; 11.17 GiB total capacity; 9.70 GiB already allocated; 179.81 MiB free; 9.85 GiB reserved in total by PyTorch) وفيما يلي بعض الحلول المحتملة التي يمكننا تجربتها لتقليل استخدام الذاكرة: تقليل حجم الدفعة المتمثل بالقيمة per_device_train_batch_size في الصنف TrainingArguments استخدام تقنية gradient_accumulation_steps في الصنف TrainingArguments لزيادة حجم الدفعة الإجمالي بفعالية، حيث تسمح لنا هذه التقنية باستخدام دفعات أصغر أثناء التدريب مع تراكم التدرجات عبر عدة دفعات ملاحظة: اطلع على دليل الأداء على منصة Huggingface لمزيد من التفاصيل حول تقنيات توفير الذاكرة. تعذر تحميل نموذج تنسرفلو المحفوظ يحفظ التابع model.save في إطار عمل تنسرفلو TensorFlow النموذج بالكامل متضمنًا البنية والأوزان وضبط التدريب في ملف واحد، ولكن قد نواجه خطأ عند محاولة تحميل ملف النموذج مرة أخرى لأن مكتبة المحولات Transformers قد لا تحمّل جميع العناصر المرتبطة بإطار عمل تنسرفلو TensorFlow في ملف النموذج. يُوصَى باتباع الخطوات التالية لتجنب المشكلات المتعلقة بحفظ وتحميل نماذج TensorFlow: حفظ أوزان النموذج مع لاحقة الملف h5 باستخدام model.save_weights، ثم إعادة تحميل النموذج باستخدام التابع from_pretrained() كما يلي: >>> from transformers import TFPreTrainedModel >>> from tensorflow import keras >>> model.save_weights("some_folder/tf_model.h5") >>> model = TFPreTrainedModel.from_pretrained("some_folder") حفظ النموذج باستخدام ~TFPretrainedModel.save_pretrained وتحميله مرة أخرى باستخدام التابع from_pretrained(): >>> from transformers import TFPreTrainedModel >>> model.save_pretrained("path_to/model") >>> model = TFPreTrainedModel.from_pretrained("path_to/model") هذا يجعل عملية الحفظ أكثر توافقًا مع مكتبة المحولات عند تحميل النماذج مرة أخرى. خطأ الاستيراد ImportError يوجد خطأ شائع آخر قد نواجهه وهو خطأ الاستيراد ImportError الذي يحدث عند محاولة استيراد مكتبة أو كائن معين، ولكن النظام لا يستطيع العثور عليه في المكان المحدد، ويقع هذا الخطأ خاصةً عند إصدار نموذج حديث ولكننا لا نزال نستخدم إصدار قديم من المكتبة لا يدعم ميزاته الجديدة. على سبيل المثال، إذا حاولنا استيراد الصنف ImageGPTImageProcessor من المكتبة transformers ولم يستطع النظام العثور عليه ستظهر رسالة خطأ كالتالي: ImportError: cannot import name 'ImageGPTImageProcessor' from 'transformers' (unknown location) لحل هذا النوع من الأخطاء، علينا التأكد من تثبيت أحدث إصدار من مكتبة Transformers للوصول إلى أحدث النماذج من خلال الأمر التالي: pip install transformers --upgrade خطأ CUDA يحدث هذا الخطأ عادةً أثناء تنفيذ العمليات الحسابية على وحدة معالجة الرسوميات GPU، وهو يتعلق بخطأ عام في الشيفرة البرمجية للجهاز ويعرض رسالة كالتالي: RuntimeError: CUDA error: device-side assert triggered لحل هذا الخطأ علينا محاولة تشغيل الشيفرة البرمجية على وحدة المعالجة المركزية CPU أولًا للحصول على رسالة خطأ توصيفية واضحة، لذا نضيف متغير البيئة التالي لبداية شيفرتنا البرمجية للتبديل إلى وحدة المعالجة المركزية: >>> import os >>> os.environ["CUDA_VISIBLE_DEVICES"] = "" كما يوجد خيار آخر يساعدنا على تشخيص الحل، وهو الحصول على تعقّب أفضل أثناء استخدام وحدة معالجة الرسوميات GPU، لذا نضيف متغير البيئة التالي إلى بداية الشيفرة البرمجية لجعل التعقّب يشير بوضوح إلى مصدر الخطأ كالتالي: >>> import os >>> os.environ["CUDA_LAUNCH_BLOCKING"] = "1" سيتسبب هذا الأمر في تمكين الوضع المتزامن، حيث تُنفذ العمليات على GPU بشكل متسلسل خطوة بخطوة، بدلاً من العمل على التوازي وبهذا يمكننا تحديد موقع الخطأ بشكل أفضل. خرج خاطئ بسبب خطأ بالتعامل مع رموز الحشو Padding Tokens عند تدريب النماذج على نصوص بأطوال مختلفة قد نحتاج لاستخدام رموز الحشو Padding Tokens وهي رموز ليس لها معنى في البيانات المدخلة ولكنها تُستخدم فقط لتعبئة السلاسل النصية بحيث يكون طولها متساوي، وتكون قيمة هذه الرموز عادةً صفرًا أو أي قيمة أخرى تحددها أثناء التدريب. في حال لم نتعامل مع هذه الرموز بشكل صحيح باستخدام ما يسمى قناع الانتباه attention mask والذي يحدد ما هي الرموز التي يجب على النموذج تجاهلها فقد نحصل على خرج غير صحيح، وبما أن مكتبة Transformers قد لا تقوم تلقائيًا بإنشاء قناع لبعض النماذج، لذا يجب إضافته يدويًا لتجنب أخطاء كهذه. على سبيل المثال قد يكون التمثيل الرقمي لدخل النموذج hidden_state غير صحيح إذا كانت معرّفات الدخل input_ids تتضمن رموز الحشو ولا تتجاهلها بشكل صحيح. لتوضيح ذلك، لنحمّل نموذجًا Model ومرمِّزًا Tokenizer، حيث يمكن الوصول إلى معرّف pad_token_id الخاص بالنموذج لمعرفة قيمته، قد تكون قيمة معرّف pad_token_id هي None لبعض النماذج وهذا يعني أن النموذج لا يستخدم رموز حشو، ولكن يمكن ضبطها يدويًا. لنستورد نموذج BERT المدرب مسبقًا والذي يحدد معرّف الحشو pad_token_id لتكون صفر : >>> from transformers import AutoModelForSequenceClassification >>> import torch >>> model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-base-uncased") >>> model.config.pad_token_id 0 سيكون الخرج الذي نحصل عليه للتسلسل الأول بدون تقنيع رموز الحشو كالتالي: >>> input_ids = torch.tensor([[7592, 2057, 2097, 2393, 9611, 2115], [7592, 0, 0, 0, 0, 0]]) >>> output = model(input_ids) >>> print(output.logits) tensor([[ 0.0082, -0.2307], [ 0.1317, -0.1683]], grad_fn=<AddmmBackward0>) ويكون الخرج الفعلي للتسلسل الثاني : >>> input_ids = torch.tensor([[7592]]) >>> output = model(input_ids) >>> print(output.logits) tensor([[-0.1008, -0.4061]], grad_fn=<AddmmBackward0>) يجب توفير قناع انتباه attention_mask لنموذجنا لتجاهل رموز الحشو وتجنب هذا الخطأ الخفي، فهو لا يعطينا رسالة خطأ صريحة، سيتطابق الآن خرج التسلسل الثاني مع الخرج الفعلي: >>> attention_mask = torch.tensor([[1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0]]) >>> output = model(input_ids, attention_mask=attention_mask) >>> print(output.logits) tensor([[ 0.0082, -0.2307], [-0.1008, -0.4061]], grad_fn=<AddmmBackward0>) لا تنشئ مكتبة المحولات Transformers قناع انتباه attention_mask تلقائيًا لرمز الحشو دائمًا وذلك لأن: بعض النماذج لا تحتوي على رمز حشو في بعض الحالات، نحتاج للاهتمام برموز الحشو وأخذها بعين الاعتبار أثناء المعالجة خطأ استخدام نموذج غير مناسب للمهمة من المهم التأكد من استخدام النموذج المناسب لكل مهمة محددة، فإذا جربنا تحميل نموذج غير متوافق مع المهمة ستظهر رسالة خطأ مشابهة للتالي: ValueError: Unrecognized configuration class XYZ for this kind of AutoModel يوصى باستخدام الصنف AutoModel لتحميل نسخ مدربة مسبقًا من النماذج، حيث يساعد هذا الصنف في تحديد وتحميل البنية الصحيحة تلقائيًا من نقطة تحقق معينة بناءً على الضبط، فإذا ظهر الخطأ عند تحميل نموذج من نقطة تحقق، فهذا يعني أن الصنف التلقائي لم يتمكن من العثور على ربط صحيح بين الضبط ونوع النموذج الذي نحاول تحميله، ولا يدعم النموذج الذي نحاول تحميله المهمة المطلوبة. على سبيل المثال، سنرى هذا الخطأ إذا حاولنا استخدام نموذج GPT2 للإجابة على الأسئلة، لأن GPT2 ليس مخصصًا لهذه المهمة. >>> from transformers import AutoProcessor, AutoModelForQuestionAnswering >>> processor = AutoProcessor.from_pretrained("openai-community/gpt2-medium") >>> model = AutoModelForQuestionAnswering.from_pretrained("openai-community/gpt2-medium") ValueError: Unrecognized configuration class <class 'transformers.models.gpt2.configuration_gpt2.GPT2Config'> for this kind of AutoModel: AutoModelForQuestionAnswering. Model type should be one of AlbertConfig, BartConfig, BertConfig, BigBirdConfig, BigBirdPegasusConfig, BloomConfig, ... الخاتمة حاولنا في هذا المقال تسليط الضوء على أبرز المشكلات التي قد نواجهها عند التعامل مع مكتبة المحوِّلات Transformers، قد لا يحتوي المقال على جميع المشكلات لكن في حال واجهتك مشكلة ما وصعب عليك حلها لا تتردد في كتابة مشكلتك في قسم الأسئلة والأجوبة في أكاديمية حسوب حيث سيجيبك عدد من المختصين عليها بالتفصيل، كما يمكنك أيضًا طلب المساعدة في منتديات منصة Huggingface التي تتضمن فئات محددة يمكنك نشر سؤالك فيها مثل فئة المبتدئين أو Transformers، وتأكّد من كتابة وصف جيد لمشكلتك مع توفير بعض الأكواد البرمجية. وعند وجود خطأ يتعلق بالمكتبة Transformers بلغ عنها في مستودع المكتبة، وحاول تضمين أكبر قدر ممكن من المعلومات التي تصف الخطأ للمساعدة على معرفته بصورة أفضل وإصلاحه بسرعة وسهولة. ترجمة -وبتصرّف- للقسم Troubleshoot من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: قياس أداء نماذج المحولات Transformers تثبيت مكتبة المحوّلات Transformers قياس أداء نماذج المحولات Transformers تعرف على منصة تنسرفلو TensorFlow للذكاء الاصطناعي
-
تعرّفنا في المقال السابق على طريقة استيراد كائنات ثلاثية الأبعاد لمحرك الألعاب جودو وكيفية ترتيبها في مشهد اللعبة، وسنضيف في هذا المقال مزيدًا من الكائنات إلى مشهد اللعبة، وسنشرح طريقة إنشاء شخصية ثلاثية الأبعاد يتحكم فيها المستخدم. بناء المشهد سنستمر في هذا المقال استخدام مجموعة الملحقات assets التي توفرها منصة Kenney على هذا الرابط والتي شرحنا طريقة تنزيلها واستيرادها في مقال سابق. نفتح مشروع اللعبة ثلاثية الأبعاد التي بدأناها، ونحدّد الآن جميع ملفات block*.glb، وننتقل إلى التبويب استيراد Import ونضبط نوع الجذر Root Type الخاص بهذه الملفات على StaticBody3D ، بعدها ننقر على زر إعادة الاستيراد Reimport كما في الصورة أدناه. بعدها، نحدّد الكائن block-grass-large.glb بزر الفأرة الأيمن ونختار إنشاء مشهد موروث جديد، ستظهر عقدة جديدة باسم block-grass-large في المشهد، وعقدة ابن لها تُمثّل المجسم Mesh، نحدد العقدة الابن وننقر فوق خيار مجسم Mesh في القائمة العلوية الظاهرة في محرر جودو، ثم نحدد خيار إنشاء شكل تصادم Create Collision Shape ونعيّن قيمة الحقل Collision Shape Placement لتكون Sibling وقيمة الحقل Collision Shape Type لتكون Trimmesh كما فعلنا بالضبط في المقال السابق. عند الضغط على زر الإنشاء سيضيف جودو تلقائيًا العقدة CollionShape3D مع شكل تصادم يطابق المجسم Mesh. يمكننا الآن حفظ المشهد باسم BlockLarge، ويفضل إنشاء مجلد منفصل وليكن platform_objects لحفظ المشهد وكافة المشاهد الأخرى التي تمثل أجزاء منصة اللعبة بأشكالها المختلفة. نفتح مشهد الأرضية Ground مع الصناديق Crates الذي عملنا عليه في المقال السابق ونحذف كافة الصناديق المضافة ونستبدلها بالمشهد الموروث الذي حفظناه للتو، بهذا سنتمكّن من وضع عدة كتل بجانب بعضها البعض بحيث تكون في صف واحد وتشكل منصة اللعبة أو عالم اللعبة. بعدها نحدّد العقدة BlockLarge وننقر خيار تعديل المحاذاة Configure Snap من القائمة العلوية التحوّل Transform الظاهرة أعلى نافذة العرض كالتالي: نضبط خيار ترجمة المحاذاة Translate Snap على القيمة 0.5، ثم ننقر على زر استخدام المحاذاة Snap Mode أو نضغط على مفتاح Y، ثم نكرّر الكتلة عدة مرات ونسحبها لترتيبها ضمن المشهد بالشكل المناسب. يمكن أيضًا إضافة مشاهد لبعض كتل المنصة الأخرى وترتيبها في أيّ شكل نريده. إضافة شخصية Character سننشئ الآن شخصية يمكنها التجول على المنصة التي أنشأناها، لذا نفتح مشهدًا جديدًا ونبدأ باستخدام العقدة CharacterBody3D بالاسم Character، حيث تتصرف عقدة PhysicsBody بطريقة مشابهة جدًا لنظيرتها ثنائية الأبعاد، وتحتوي على التابع move_and_slide() الذي سنستخدمه لإجراء الحركة وكشف التصادم. نضيف عقدة MeshInstance3D على شكل كبسولة، وعقدة CollionShape3D مطابقة لها، ولنتذكّر أن بإمكاننا إضافة عقدة StandardMaterial3D إلى المجسم Mesh وضبط خاصية اللون Color في القسم Albedo. شكل الكبسولة جميل ومناسب للشخصية، ولكن سيكون من الصعب معرفة الاتجاه الذي يمثل الوجه، لذا لنضيف مجسم Mesh آخر على شكل مخروطي CylinderMesh3D، ونضبط نصف قطره العلوي Top Radius على القيمة 0.2 ونصف قطره السفلي Bottom Radius على القيمة 0.001 وارتفاعه Height على القيمة 0.5، ثم نضبط دورانه حول المحور x على -90 درجة. أصبح لدينا الآن شكل مخروطي جميل، لذا نرتّبه بحيث يشير لخارج الجسم على طول محور z السالب، إذ يمكن بسهولة معرفة الاتجاه السالب لأن أسهم أداة Gizmo تشير إلى الاتجاه الموجب. ملاحظة: أضفنا في هذه الشخصية أيضًا مجسمين كرويين بنفس الطريقة لتمثيل عيني الشخصية، ويمكن إضافة أية تفاصيل نريدها. لنضف الآن عقدة كاميرا Camera3D إلى المشهد لتتبع الشخصية. نضع الكاميرا خلف الشخصية وفوقها مع توجيهها للأسفل قليلًا، ثم ننقر على زر معاينة Preview للتحقق من عرض الكاميرا وظهورالشخصية بالشكل المناسب. كود التحكم في الشخصية قبل إضافة سكربت برمجي للتحكم في الشخصية، سوف نفتح إعدادات المشروع Project Settings، ونضيف المدخلات التالية في تبويب خريطة الإدخال Input Map: إجراء الإدخال Input Action المفتاح Key التحرك للأمام move_forward المفتاح W التحرك للخلف move_back المفتاح S الانعطاف لليمين strafe_right المفتاح D الانعطاف لليسار strafe_left المفتاح A القفز jump المسافة Space يمكننا الآن كتابة سكربت لتحريك شخصيتنا ثلاثية الأبعاد كما يلي: extends CharacterBody3D var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") var speed = 4.0 # سرعة الحركة var jump_speed = 6.0 # تحديد ارتفاع القفزة var mouse_sensitivity = 0.002 # سرعة الدوران func get_input(): var input = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_back") velocity.x = input.x * speed velocity.z = input.y * speed func _physics_process(delta): velocity.y += -gravity * delta get_input() move_and_slide() نلاحظ أن الشيفرة البرمجية في الدالة _physics_process() بسيطة، حيث نضيف الجاذبية للتسارع في الاتجاه الموجب للمحور Y إلى الأسفل، ثم نستدعي الدالة get_input() للتحقق من الإدخال أي معرفة المفتاح الذي ضغط المستخدم عليه، ثم نستخدم التابع move_and_slide() للتحرك في اتجاه متجه السرعة. نشغّل اللعبة لاختبارها، وسنرى النتيجة التالية: هذا يبدو جيدًا، ولكن يجب أن نكون قادرين على تدوير الشخصية باستخدام الفأرة، لذا سنضيف الشيفرة البرمجية التالية إلى سكربت الشخصية: func _unhandled_input(event): if event is InputEventMouseMotion: rotate_y(-event.relative.x * mouse_sensitivity) عندما نحرك الفأرة أفقيًا في اتجاه المحور x سيعمل السكربت بتدوير الشخصية حول المحور الرأسي Y، وبالتالي إذا حركنا الفأرة إلى اليمين، سيؤدي ذلك إلى تدوير الشخصية إلى اليسار. وإذا حركنا الفأرة إلى اليسار ستدور الشخصية إلى اليمين. لدينا هنا مشكلة، إذ يحرّكنا الضغط على مفتاح W على طول المحور Z لعالم اللعبة بغض النظر عن الاتجاه الذي تتجه له الشخصية، لأن اللعبة تستخدم هنا الإحداثيات العالمية Global Coordinates، ولكننا بحاجة إلى التحرك بناء على اتجاه الكائن، ويمكن حل هذه المشكلة باستخدام التحويلات. الاستفادة من التحويلات Transforms التحويل هو مصفوفة رياضية تحتوي على معلومات حول انتقال الكائن ودورانه وتغيير حجمه في آن واحد، ويُخزَّنه جودو في نوع البيانات Transform، حيث تُسمَّى معلومات الموضع بالاسم transform.origin وتكون معلومات الاتجاه في transform.basis. تشير محاور X و Y و Z الخاصة بأداة Gizmo على طول محاور الكائن نفسه عندما تكون في وضع الحيّز المحلي Local Space Mode، ويشابه ذلك الأساس basis الخاص بالتحويل، حيث يحتوي هذا الأساس على ثلاثة كائنات Vector3 تسمى x و y و z تمثل هذه الاتجاهات، ويمكننا استخدامها لضمان أن الضغط على مفتاح W سيؤدي إلى التحرك في الاتجاه الأمامي للكائن دائمًا. نعدّل الدالة get_input() كما يلي: func get_input(): var input = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_back") var movement_dir = transform.basis * Vector3(input.x, 0, input.y) velocity.x = movement_dir.x * speed velocity.z = movement_dir.z * speed نطبّق هذا التحويل على المتجه من خلال ضرب متجه الإدخال في أساس التحويل transform.basis. يمثّل الأساس دوران الكائن، لذا لنحوّل الآن اتجاه الأمام والخلف للإشارة على طول المحور Z للكائن، ونحوّل مفاتيح التحريك لليمين واليسار للإشارة على طول المحور X الخاص بالكائن. القفز Jumping سنضيف الآن حركة أخرى إلى اللاعب وهي القفز، لتحقيق ذلك نضيف الأسطر التالية إلى نهاية الدالة _unhandled_input(): if event.is_action_pressed("jump") and is_on_floor(): velocity.y = jump_speed تحسين الكاميرا إذا وقفت الشخصية بالقرب من عائق ما، فيمكن للكاميرا الالتصاق بالكائن بطريقة غير لطيفة. وبالرغم من أن برمجة كاميرا ثلاثية الأبعاد قد تكون أمرًا معقدًا بحد ذاته، ولكن يمكننا استخدام عقد جودو المضمنة للحصول على حل جيد لذلك. نحذف العقدة Camera3D من مشهد الشخصية ونضيف العقدة SpringArm3D التي تعمل كذراع متحركة تحمل الكاميرا أثناء كشف التصادمات، وستقرّب الكاميرا عند وجود عقبة، ثم نضبط خاصية طول الذراع Spring Length على القيمة 5، ونضبط خاصية الموضع Position على القيم (0, 1, 0). نلاحظ ظهور خط بلون أصفر يشير إلى طول النابض Spring Length، حيث ستتحرك الكاميرا على طول هذا الخط، ولكنها ستقترب عند وجود عقبة حتى نهايته كلما أمكن ذلك. نعيد إضافة العقدة Camera3D كابن للعقدة SpringArm3D، ونحاول تشغيل اللعبة مرة أخرى، ويمكن تجربة تدوير ذراع النابض حول محوره X ليشير إلى الأسفل قليلًا حتى الوصول لنتيجة مرضية. الخلاصة شرحنا في هذا المقال كيفية بناء مشهد ثلاثي الأبعاد أكثر تعقيدًا وكتابة الشيفرة البرمجية لحركة شخصية يتحكم فيها المستخدم، وتعلمنا أيضًا مفهوم التحويلات Transforms التي تعد مفهومًا مهمًا جدًا في الألعاب ثلاثية الأبعاد، والتي ستستخدمها بكثرة في المقالات القادمة. ترجمة -وبتصرّف- للقسم Creating a 3D Character من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: استيراد الكائنات ثلاثية الأبعاد في جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو استخدام محرر جودو ثلاثي الأبعاد كتابة سكربتات GDScript وإرفاقها بالعقد في جودو
-
تفيدنا لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- في إدارة البيانات المخزنة في نظام إدارة قواعد بيانات علاقية Relational Database Management System -أو RDBMS اختصارًا- ومن أبرز الوظائف المفيدة في SQL هي إنشاء استعلام ضمن استعلام آخر، والذي يُعرَف باسم الاستعلام الفرعي Subquery أو الاستعلام المتداخل Nested وهو موضوع مقالنا اليوم. متطلبات العمل يجب توفر حاسوب يشغّل أحد أنواع أنظمة إدارة قواعد البيانات العلاقية RDBMS التي تستخدم لغة SQL. وبالنسبة لهذا المقال فقد اختبرنا التعليمات والأمثلة الواردة فيه باستخدام البيئة التالية: خادم عامل على أحدث إصدار من توزيعة أوبنتو Ubuntu مع مستخدم ذو صلاحيات مسؤول مختلف عن المستخدم الجذر، وجدار حماية مُفعَّل كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضّح في مقال كيفية تثبيت MySQL على أوبنتو وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة 3 من المقال. ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن قد نجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على جدول يحتوي بيانات تجريبية نموذجية لنتمكّن من التدرب على استخدام الاستعلامات المتداخلة، وفي حال لم تكن متوفرة فيمكن مطالعة القسم التالي لمعرفة كيفية إنشاء قاعدة البيانات والجدول المستخدَمَين في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية إذا كانت قاعدة بيانات SQL الخاصة بنا تعمل على خادم بعيد، علينا الاتصال بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم zooDB: mysql> CREATE DATABASE zooDB; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات zooDB من خلال تنفيذ تعليمة USE التالية: mysql> USE zooDB; الخرج Database changed اخترنا قاعدة البيانات، وسننشئ جدولًا ضمنها، حيث سننشئ جدولًا يخزّن المعلومات حول الزوار الذين يزورون حديقة الحيوان، وسيحتوي هذا الجدول على الأعمدة السبعة التالية: guest_id: يخزّن قيمًا للزوار الذين يزورون حديقة الحيوان ويستخدم نوع البيانات int، ويمثّل المفتاح الرئيسي Primary Key للجدول، ممّا يعني أن كل قيمة في هذا العمود ستمثّل معرّفًا فريدًا للصف الخاص بها first_name: الاسم الأول لكل زائر ويستخدم نوع البيانات varchar بحد أقصى 30 محرف last_name: الاسم الأخير لكل زائر ويستخدم نوع البيانات varchar بحد أقصى 30 محرف guest_type: يحدد هل الزائر بالغ أو طفل ويستخدم نوع البيانات varchar بحد أقصى 15 محرف membership_type: نوع العضوية لكل زائر ويستخدم نوع بيانات varchar بحد أقصى 30 محرف membership_cost: تكلفة أنواع العضوية المختلفة، ويستخدم نوع البيانات decimal بدقة 5 ومقياس 2، أي أن القيم الموجودة في هذا العمود يمكن أن تحتوي على خمسة أرقام مع وجود رقمين على يمين الفاصلة العشرية total_visits: يسجل إجمالي عدد الزيارات لكل زائر ويستخدم نوع البيانات int سننشئ جدولًا باسم guests يحتوي على الأعمدة السابقة من خلال تشغيل الأمر CREATE TABLE التالي: mysql> CREATE TABLE guests ( mysql> guest_id int, mysql> first_name varchar(30), mysql> last_name varchar(30), mysql> guest_type varchar(15), mysql> membership_type varchar(30), mysql> membership_cost decimal(5,2), mysql> total_visits int, mysql> PRIMARY KEY (guest_id) mysql> ); ثم ندخل بعض البيانات التجريبية في هذا الجدول الفارغ كما يلي: mysql> INSERT INTO guests mysql> (guest_id, first_name, last_name, guest_type, membership_type, membership_cost, total_visits) mysql> VALUES mysql> (1, 'Judy', 'Hopps', 'Adult', 'Resident Premium Pass', 110.0, 168), mysql> (2, 'Nick', 'Wilde', 'Adult', 'Day Pass', 62.0, 1), mysql> (3, 'Duke', 'Weaselton', 'Adult', 'Resident Pass', 85.0, 4), mysql> (4, 'Tommy', 'Yax', 'Child', 'Youth Pass', 67.0, 30), mysql> (5, 'Lizzie', 'Yax', 'Adult', 'Guardian Pass', 209.0, 30), mysql> (6, 'Jenny', 'Bellwether', 'Adult', 'Resident Premium Pass', 110.0, 20), mysql> (7, 'Idris', 'Bogo', 'Child', 'Youth Pass', 67.0, 79), mysql> (8, 'Gideon', 'Grey', 'Child', 'Youth Pass', 67.0, 100), mysql> (9, 'Nangi', 'Reddy', 'Adult', 'Guardian Champion', 400.0, 241), mysql> (10, 'Octavia', 'Otterton', 'Adult', 'Resident Pass', 85.0, 11), mysql> (11, 'Calvin', 'Roo', 'Adult', 'Resident Premium Pass', 110.0, 173), mysql> (12, 'Maurice', 'Big', 'Adult', 'Guardian Champion', 400.0, 2), mysql> (13, 'J.K.', 'Lionheart', 'Child', 'Day Pass', 52.0, 1), mysql> (14, 'Priscilla', 'Bell', 'Child', 'Day Pass', 104.0, 2), mysql> (15, 'Tommy', 'Finnick', 'Adult', 'Day Pass', 62.0, 1); الخرج Query OK, 15 rows affected (0.01 sec) Records: 15 Duplicates: 0 Warnings: 0 نحن الآن جاهزون لبدء استخدام الاستعلامات المتداخلة في لغة SQL. ما هو الاستعلام المتداخل الاستعلام في لغة SQL هو عملية تسترجع البيانات من جدول في قاعدة بيانات وتتضمن تعليمة SELECT دائمًا، أما الاستعلام المتداخل nested query فهو استعلام داخل استعلام آخر، بمعنى آخر الاستعلام المتداخل هو تعليمة SELECT توضع بين قوسين عادة، وتُضمَّن في عملية SELECT أو INSERT أو DELETE رئيسية، ويُعَد الاستعلام المتداخل مفيدًا في الحالات التي نريد فيها تنفيذ أوامر متعددة في تعليمة استعلام واحدة بدلًا من كتابة عدة أوامر لإعادة النتيجة المطلوبة، ويمكننا من خلال الاستعلامات المتداخلة إتمام عمليات معقدة على البيانات بطريقة أسهل. سنستخدم في هذا المقال الاستعلامات المتداخلة مع تعليمات SELECT و INSERT و DELETE، كما سنستخدم الدوال التجميعية Aggregate Functions ضمن استعلام متداخل لمقارنة قيم البيانات بقيم البيانات المفروزة التي حدّدناها باستخدام تعلميتي WHERE و LIKE. استخدام الاستعلامات المتداخلة مع تعليمة SELECT لنوضّح فائدة الاستعلامات المتداخلة عمليًا باستخدام البيانات التجريبية التي أضفناها في خطوة سابقة، ولنفترض مثلًا أننا نريد البحث عن جميع الزوار في الجدول guests الذين زاروا حديقة الحيوان بعدد مرات أكبر من متوسط الزيارات. قد نفترض أن بإمكاننا العثور على هذه المعلومات من خلال الاستعلام التالي: mysql> SELECT first_name, last_name, total_visits mysql> FROM guests mysql> WHERE total_visits > AVG(total_visits); ولكن سيعيد الاستعلام الذي يستخدم الصيغة السابقة خطأ كما يلي: الخرج ERROR 1111 (HY000): Invalid use of group function سبب هذا الخطأ هو أن الدوال التجميعية مثل الدالة AVG() لا تعمل إلا إذا كانت مُنفَّذة ضمن تعليمة SELECT. أحد الخيارات لاسترجاع هذه المعلومات هو تشغيل استعلام للعثور على متوسط عدد زيارات الزوار أولًا، ثم تشغيل استعلام آخر للعثور على النتائج بناءً على تلك القيمة كما هو الحال في المثالين التاليين: mysql> SELECT AVG(total_visits) FROM guests; الخرج +-----------------+ | avg(total_visits) | +-----------------+ | 57.5333 | +-----------------+ 1 row in set (0.00 sec) mysql> SELECT first_name, last_name, total_visits mysql> FROM guests mysql> WHERE total_visits > 57.5333; الخرج +----------+---------+------------+ | first_name | last_name | total_visits | +----------+---------+------------+ | Judy | Hopps | 168 | | Idris | Bogo | 79 | | Gideon | Grey | 100 | | Nangi | Reddy | 241 | | Calvin | Roo | 173 | +----------+---------+------------+ 5 rows in set (0.00 sec) ولكن يمكننا الحصول على مجموعة النتائج نفسها باستخدام استعلام واحد من خلال تداخل الاستعلام الأول (SELECT AVG(total_visits) FROM guests;) مع الاستعلام الثاني. ينبغي أن نضع في الحسبان أن استخدام العدد المناسب من الأقواس مع الاستعلامات المتداخلة أمر ضروري لإكمال العملية التي نريد تنفيذها، لأن الاستعلام المتداخل هو أول عملية تُنفَّذ: mysql> SELECT first_name, last_name, total_visits mysql> FROM guests mysql> WHERE total_visits > mysql> (SELECT AVG(total_visits) FROM guests); الخرج +------------+-----------+--------------+ | first_name | last_name | total_visits | +------------+-----------+--------------+ | Judy | Hopps | 168 | | Idris | Bogo | 79 | | Gideon | Grey | 100 | | Nangi | Reddy | 241 | | Calvin | Roo | 173 | +------------+-----------+--------------+ 5 rows in set (0.00 sec) يوضّح المثال السابق أهمية استخدام استعلام متداخل في تعليمة واحدة كاملة للحصول على النتائج المطلوبة بدلًا من الاضطرار إلى تشغيل استعلامين منفصلين، وفق الخرج الذي حصلنا عليه فقد زار خمسة زوار حديقة الحيوان بعدد مرات أكبر من متوسط الزيارات، ويمكن أن تقدّم هذه المعلومات نظرة مفيدة للتفكير في طرق إبداعية لضمان استمرار الأعضاء الحاليين في زيارة حديقة الحيوان وتجديد عضويتهم كل سنة. استخدام الاستعلامات المتداخلة مع تعليمة INSERT لا يقتصر الأمر على تضمين الاستعلام المتداخل في تعليمات SELECT أخرى فقط، إذ يمكننا أيضًا استخدام الاستعلامات المتداخلة لإدخال البيانات في جدول موجود مسبقًا من خلال تضمين الاستعلام المتداخل في عملية INSERT. لنفترض وجود حديقة حيوانات ما تطلب بعض المعلومات عن زوار حديقة الحيوان خاصة بنا وتود تقديم خصم بنسبة 15% للزوار الذين يشترون العضوية الدائمة في موقعها، لذا سنستخدم تعليمة CREATE TABLE لإنشاء جدول جديد بالاسم upgrade_guests والذي يحتوي على ستة أعمدة، وننتبه جيدًا لأنواع البيانات مثل int و varchar والحد الأقصى من المحارف التي يمكن الاحتفاظ بها، فإن لم تكن متماشية مع أنواع البيانات الأصلية من الجدول guests الذي أنشأناه في قسم إعداد قاعدة بيانات نموذجية، فسنتلقى خطأً عند محاولة إدخال بيانات من الجدول guests باستخدام استعلام متداخل ولن تُنقَل البيانات بطريقة صحيحة. لننشئ هذا الجدول مع المعلومات التالية: mysql> CREATE TABLE upgrade_guests ( mysql> guest_id int, mysql> first_name varchar(30), mysql> last_name varchar(30), mysql> membership_type varchar(30), mysql> membership_cost decimal(5,2), mysql> total_visits int, mysql> PRIMARY KEY (guest_id) mysql> ); احتفظنا بمعظم معلومات نوع البيانات في هذا الجدول كما كانت في الجدول guests من أجل التناسق والدقة، وحذفنا أي أعمدة إضافية لا نريدها في الجدول الجديد. أصبح هذا الجدول الفارغ جاهزًا للبدء، والخطوة التالية هي إدخال قيم البيانات المطلوبة في الجدول. نكتب التعليمة INSERT INTO مع الجدول upgrade_guests الجديد مع وجود اتجاه واضح لمكان إدخال البيانات، ثم نكتب الاستعلام المتداخل باستخدام تعليمة SELECT لاسترجاع قيم البيانات ذات الصلة وتعليمة FROM للتأكد من أن البيانات تأتي من الجدول guests. سيطبّق خصم بقيمة 15% على الأعضاء الدائمين من خلال تضمين عملية الضرب الرياضية * للضرب بالعدد 0.85 ضمن الاستعلام المتداخل (membership_cost * 0.85)، وتفيدنا تعليمة WHERE في فرز القيم الموجودة في العمود membership_type. يمكننا تضييق نطاق النتائج لتشمل فقط نتائج الأعضاء الدائمين باستخدام تعليمة LIKE ووضع رمز النسبة المئوية % قبل وبعد الكلمة "Resident" بين علامتي اقتباس مفردتين لتحديد نوع العضوية التي تتبع النمط أو المفردات نفسها، وسيُكتَب هذا الاستعلام كما يلي: mysql> INSERT INTO upgrade_guests mysql> SELECT guest_id, first_name, last_name, membership_type, mysql> (membership_cost * 0.85), total_visits mysql> FROM guests mysql> WHERE membership_type LIKE '%resident%'; الخرج Query OK, 5 rows affected, 5 warnings (0.01 sec) Records: 5 Duplicates: 0 Warnings: 5 يشير الخرج السابق إلى إضافة خمسة سجلات إلى الجدول upgrade_guests الجديد. يمكن التأكد من نقل البيانات التي طلبتها بنجاح من الجدول guests إلى الجدول upgrade_guests الفارغ الذي أنشأناه من خلال تشغيل الأمر التالي مع الشروط التي حدّدناها مع الاستعلام المتداخل وتعليمة WHERE: mysql> SELECT * FROM upgrade_guests; الخرج +----------+------------+------------+-----------------------+-----------------+--------------+ | guest_id | first_name | last_name | membership_type | membership_cost | total_visits | +----------+------------+------------+-----------------------+-----------------+--------------+ | 1 | Judy | Hopps | Resident Premium Pass | 93.50 | 168 | | 3 | Duke | Weaselton | Resident Pass | 72.25 | 4 | | 6 | Jenny | Bellwether | Resident Premium Pass | 93.50 | 20 | | 10 | Octavia | Otterton | Resident Pass | 72.25 | 11 | | 11 | Calvin | Roo | Resident Premium Pass | 93.50 | 173 | +----------+------------+------------+-----------------------+-----------------+--------------+ 5 rows in set (0.01 sec) نلاحظ الإدراج الصحيح لمعلومات عضوية الزائر الدائم "Resident" ذات الصلة من الجدول guest في الجدول upgrade_guests، مع إعادة حساب التكلفة membership_cost الجديدة مع تطبيق خصم 15%، وبذلك ساعدت هذه العملية في تقسيم الجمهور المناسب واستهدافه، وأصبحت الأسعار المخفَّضة متاحة بسهولة لمشاركتها مع الأعضاء الجدد المحتملين. استخدام الاستعلامات المتداخلة مع تعليمة DELETE لنفترض أننا نريد إزالة الزوار المعتادين والتركيز فقط على الترويج لخصم البطاقة المميزة للأعضاء الذين لا يزورون حديقة الحيوان كثيرًا حاليًا. نبدأ هذه العملية باستخدام تعليمة DELETE FROM بحيث يكون من الواضح المكان الذي ستحذف البيانات منه، وهو الجدول upgrade_guests في حالتنا، ثم نستخدم تعليمة WHERE لفرز أي قيمة من العمود total_visits تزيد عن الكمية المحدَّدة في الاستعلام المتداخل. نستخدم تعليمة SELECT في استعلامنا المتداخل المُضمَّن للعثور على المتوسط الحسابي AVG للعمود total_visits بحيث تحتوي تعليمة WHERE السابقة على قيم البيانات المناسبة للمقارنة معها. وأخيرًا، نستخدم تعليمة FROM لاسترجاع تلك المعلومات من الجدول guests، وسيكون الاستعلام الكامل كما يلي: mysql> DELETE FROM upgrade_guests mysql> WHERE total_visits > mysql> (SELECT AVG(total_visits) FROM guests); الخرج Query OK, 2 rows affected (0.00 sec) لنتأكّد من حذف هذه السجلات بنجاح من الجدول upgrade_guests، ونستخدم تعليمة ORDER BY لتنظيم النتائج وفق العمود total_visits بترتيب رقمي تصاعدي. ملاحظة: لن يؤدي استخدام تعليمة DELETE لحذف السجلات من جدولنا الجديد إلى حذفها من الجدول الأصلي، حيث يمكننا تشغيل التعليمة SELECT * FROM original_table للتأكد من وجود جميع السجلات الأصلية، حتى إن كانت محذوفة من الجدول الجديد. mysql> SELECT * FROM upgrade_guests ORDER BY total_visits; الخرج +----------+------------+------------+-----------------------+-----------------+--------------+ | guest_id | first_name | last_name | membership_type | membership_cost | total_visits | +----------+------------+------------+-----------------------+-----------------+--------------+ | 3 | Duke | Weaselton | Resident Pass | 72.25 | 4 | | 10 | Octavia | Otterton | Resident Pass | 72.25 | 11 | | 6 | Jenny | Bellwether | Resident Premium Pass | 93.50 | 20 | +----------+------------+------------+-----------------------+-----------------+--------------+ 3 rows in set (0.00 sec) يشير هذا الخرج إلى نجاح تعليمة DELETE والاستعلام المتداخل في حذف قيم البيانات المُحدَّدة، لذا سيحتوي هذا الجدول الآن على معلومات الزوار الثلاثة الذين عدد زياراتهم أقل من متوسط عدد الزيارات، وهو ما يُعَد نقطة انطلاق لموظف حديقة الحيوان للتواصل مع هؤلاء الزوار بشأن الترقية إلى التذاكر المميزة بسعر مخفَّض لتشجيعهم على الذهاب أكثر إلى حديقة الحيوان. الخلاصة شرحنا في هذا المقال الاستعلامات المتداخلة ووضحنا فائدتها في الحصول على نتائج دقيقة لم نكن سنتمكن من الحصول عليها إلا من خلال تشغيل استعلامات منفصلة، وعرضنا أمثلة عملية لاستخدام تعليمات SELECT وINSERT و DELETE مع الاستعلامات المتداخلة لتوفير طريقة أخرى لإدخال البيانات أو حذفها في خطوة واحدة. ترجمة -وبتصرف- للمقال How To Use Nested Queries in SQL لصاحبته Jeanelle Horcasitas. اقرأ أيضًا المقال السابق: استخدام تعليمتي GROUP BY و ORDER BY في SQL الاستعلامات الفرعية Subqueries في SQL مفاهيم نموذج البيانات العلائقية RDM الأساسية بعض الدوال المساعدة في SQL