> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# تعلم SQL (1): ما هو SQL وكيف تكتب أول استعلام
- URL: https://arabroot.io/articles/أساسيات-sql-للمطورين/
- Published: 2026-10-07T15:00:00.000Z
- Updated: 2026-10-11T06:04:44.000Z
- Description: تتعلم SQL من الصفر: ما هي قاعدة البيانات العلائقية، وكيف تنشئ الجداول وتضيف البيانات وتكتب أول استعلام بأمان.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, قواعد البيانات, أساسيات, الاستضافة الذاتية

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

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

المواضيع في السلسلة:

- **تعلم SQL (1): ما هو SQL وكيف تكتب أول استعلام** (الموضوع الحالي): قواعد البيانات العلائقية وتاريخ اللغة، وأنواع الأوامر، وإنشاء الجداول والقيود، والإضافة والتعديل والحذف، والاستعلام SELECT والدوال والقيمة NULL.
- [تعلم SQL (2): الربط JOIN والتجميع والاستعلامات الفرعية](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-2-%D8%A7%D9%84%D8%B1%D8%A8%D8%B7-join-%D9%88%D8%A7%D9%84%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D9%84%D8%A7%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D8%B9%D9%8A%D8%A9/): أنواع الربط كلها، والتجميع GROUP BY وHAVING، والاستعلامات الفرعية Subqueries، وعمليات المجموعات UNION وINTERSECT وEXCEPT.
- [تعلم SQL (3): CTE ودوال النوافذ والمعاملات](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/): جمل WITH والاستعلام التكراري، ودوال النوافذ Window Functions، والعروض Views، والإضافة أو التحديث UPSERT، وJSON، والمعاملات ومستويات العزل، وحقن SQL.

وسوف نتناول في هذا الجزء ما يلي:

- ما هي قاعدة البيانات العلائقية، وما معنى الجدول والصف والعمود والمفتاح الأساسي والمفتاح الأجنبي.
- من أين جاءت SQL، من ورقة Codd عام 1970 حتى المعيار SQL:2023، والمحركات المشهورة ولهجاتها.
- المجموعات الخمس من الأوامر DDL وDML وDQL وDCL وTCL، ومثال من كل واحدة.
- أنواع البيانات، وإنشاء الجداول وتعديلها وحذفها، والقيود Constraints التي تمنع البيانات الخاطئة.
- الإضافة INSERT والتعديل UPDATE والحذف DELETE بأمان داخل معاملة.
- الاستعلام SELECT مع WHERE وORDER BY وLIMIT وDISTINCT، ثم الدوال والعبارة CASE والقيمة NULL والمنطق الثلاثي.
- الترتيب الذي تقرأ به قاعدة البيانات الجملة، والأخطاء الشائعة في البداية، وتمارين مع حلولها.

وأما تصميم الجداول نفسها والتطبيع Normalization فلهما مقال مستقل هو [تصميم قواعد البيانات](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/)، والمقارنة بين أنواع قواعد البيانات العلائقية وغير العلائقية في أنواع قواعد البيانات ومتى تختار كل نوع، لذلك لن نعيد شرحها هنا.

## ما هي قاعدة البيانات العلائقية؟

قاعدة البيانات Database هي مجموعة منظمة من البيانات يحفظها برنامج متخصص يسمى نظام إدارة قواعد البيانات Database Management System واختصاراً DBMS، مثل PostgreSQL وMySQL وSQL Server، وهذا البرنامج هو الذي يكتب البيانات على القرص ويقرؤها ويمنع تطبيقين من إفساد بعضهما ويعيد البيانات سليمة بعد انقطاع الكهرباء. وقد يتساءل البعض: لماذا لا أحفظ بياناتي في ملف JSON أو Excel وأنتهي؟ والإجابة أن الملف يكفي ما دام شخص واحد يقرؤه ويكتبه، أما عندما يكتب مئة مستخدم في نفس اللحظة، ويريد أحدهم تقريراً من مليون صف، ويجب ألا يضيع أي طلب إذا توقف السيرفر في منتصف الكتابة، فأنت تحتاج برنامجاً صمم لهذا العمل بالضبط.

والنوع الأشهر هو قاعدة البيانات العلائقية Relational Database، وهي تحفظ البيانات في جداول Tables، والفكرة قريبة جداً من جدول Excel الذي تعرفه:

- الجدول Table: مجموعة من الأشياء من نفس النوع، مثل جدول الطلاب students وجدول الدورات courses.
- الصف Row ويسمى أيضاً السجل Record: شيء واحد، مثل الطالبة سارة بكل بياناتها.
- العمود Column: معلومة واحدة لكل الصفوف ولها نوع ثابت، مثل الاسم name من نوع نص، وتاريخ التسجيل joined\_on من نوع تاريخ.
- المفتاح الأساسي Primary Key: عمود قيمته مختلفة في كل صف ولا تكون فارغة، فيعرف به الصف دون لبس، مثل `id`، فقد يوجد طالبان باسم Sara ولكن لا يوجد طالبان بنفس الرقم.
- المفتاح الأجنبي Foreign Key: عمود في جدول يشير إلى المفتاح الأساسي في جدول آخر، مثل `student_id` في جدول التسجيلات الذي يشير إلى رقم الطالب، وقاعدة البيانات تمنع أن يشير إلى طالب غير موجود.

والكلمة «علائقية» لا تأتي من العلاقات بين الجداول كما يظن الكثير، وإنما من المصطلح الرياضي العلاقة Relation الذي سمى به Codd الجدول في ورقته، فالجدول في نموذجه هو مجموعة من الصفوف لها نفس الأعمدة، وأهم فكرة فيه أن الصفوف ترتبط ببعضها عن طريق القيم نفسها، فالتسجيل يعرف طالبه لأن فيه رقم الطالب، وليس لأنه مربوط به بمؤشر في الذاكرة. وهذه الفكرة هي التي سمحت لاحقاً بلغة مثل SQL، تسأل فيها عن البيانات بقيمها دون أن تعرف أين تخزن ولا كيف تصل إليها.

وفي هذه السلسلة سوف نعمل على قاعدة بيانات لمعهد تدريب اسمها `academy` فيها خمسة جداول: المدربون instructors، والدورات courses، والطلاب students، والتسجيلات enrollments التي تربط الطالب بالدورة وتحفظ درجته إن انتهى منها، والدفعات payments التي دفعها الطالب عن كل تسجيل. والبيانات صغيرة عن قصد، والسبب أنك تستطيع أن ترى كل صف بعينك فتحسب النتيجة في رأسك قبل أن تشغل الاستعلام، وهذه أهم عادة تتعلمها في SQL. والصورة التالية تبين الجداول الخمسة وأعمدتها والمفاتيح التي تربطها:

![خمسة جداول: instructors بالأعمدة id وname وmanager_id وفيه ستة صفوف، وcourses بالأعمدة id وtitle وtrack وprice وcapacity وinstructor_id وفيه خمسة صفوف، وenrollments بالأعمدة id وstudent_id وcourse_id وenrolled_on وgrade وفيه تسعة صفوف، وstudents بالأعمدة id وname وcity وemail وphone وjoined_on وفيه ستة صفوف، وpayments بالأعمدة id وenrollment_id وamount وpaid_on وفيه عشرة صفوف. وأسهم المفاتيح الأجنبية: manager_id إلى instructors.id، وinstructor_id إلى instructors.id مع ON DELETE SET NULL، وcourse_id إلى courses.id، وstudent_id إلى students.id، وenrollment_id إلى enrollments.id مع ON DELETE CASCADE، وعلامات PK وFK وUNIQUE وCHECK وNULL ok بجانب الأعمدة](https://arabroot.io/content/images/2026/10/sql-basics-01-schema.webp)

كل سهم مفتاح أجنبي يشير إلى المفتاح الأساسي في جدول آخر، وجدول المدربين يشير إلى نفسه لأن لكل مدرب مديراً

## من أين جاءت لغة SQL؟

في يونيو 1970 نشر Edgar F. Codd، وهو رياضي يعمل في مختبر IBM للأبحاث في مدينة San Jose، ورقة بعنوان [A Relational Model of Data for Large Shared Data Banks](https://www.seas.upenn.edu/~zives/03f/cis550/codd.pdf?ref=arabroot.io) في مجلة Communications of the ACM، واقترح فيها النموذج العلائقي الذي شرحناه في الجزء السابق، وكما تذكر [صفحة IBM عن Codd](https://www.ibm.com/history/edgar-codd?ref=arabroot.io) فقد كانت الفكرة في ذلك الوقت نظرية فقط. وفي عام 1973 بدأ في IBM مشروع اسمه System R لبناء نظام حقيقي على هذا النموذج، وكان في فريقه Donald Chamberlin وRaymond Boyce اللذان صمما لغة للاستعلام سمياها Structured English Query Language واختصاراً SEQUEL، ونشرا ورقتها في مايو 1974، وتوفي Boyce في نفس العام، وهذه التفاصيل يرويها Chamberlin بنفسه في [نص لقاء رواد System R عام 1995](https://www.mcjones.org/System%5FR/SQL%5FReunion%5F95/SRC-1997-018.pdf?ref=arabroot.io).

ويروي Chamberlin في نفس اللقاء سبب تغيير الاسم: فقد اعترضت شركة الطائرات البريطانية Hawker Siddeley بأن SEQUEL علامة تجارية مسجلة لها، فحذف Chamberlin حروف العلة من الاسم فصار SQL، ولهذا ما زال كثير من المطورين ينطقونه «سيكوال» حتى اليوم، ويكتب [توثيق Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/History-of-SQL.html?ref=arabroot.io) نفسه أن الاسم ما زال ينطق «sequel»، بينما ينطقه آخرون حرفاً حرفاً «إس كيو إل»، والاثنان مقبولان في الشغل.

وبعد ذلك خرجت اللغة من المختبر إلى السوق، فبحسب [توثيق Oracle](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/History-of-SQL.html?ref=arabroot.io) قدمت شركة Relational Software، التي أصبح اسمها Oracle لاحقاً، أول تطبيق تجاري للغة SQL عام 1979، وأصدرت IBM قاعدة البيانات DB2 عام 1983 كما تذكر [صفحة IBM عن قواعد البيانات العلائقية](https://www.ibm.com/history/relational-database?ref=arabroot.io). ولأن كل شركة بدأت تضيف ما تريد، احتاج السوق إلى معيار واحد، فاعتمد المعهد الأمريكي ANSI أول معيار للغة عام 1986، ثم اعتمدته المنظمة الدولية ISO عام 1987 تحت الرقم ISO 9075، وتوالت الإصدارات بعده كما يلخصها [جدول تاريخ المعيار](https://en.wikipedia.org/wiki/SQL?ref=arabroot.io#Standardization%5Fhistory):

- SQL-92: مراجعة كبيرة للمعيار الأول، وهي الأساس الذي ما زالت أغلب الكتب تشرح منه.
- SQL:1999: أضاف الاستعلامات التكرارية Recursive Queries والقوادح Triggers.
- SQL:2003: أضاف دوال النوافذ Window Functions والأعمدة التي تولد قيمها تلقائياً Identity Columns.
- SQL:2011: أضاف البيانات الزمنية Temporal Data التي تحفظ تاريخ تغير الصف.
- SQL:2016: أضاف دوال JSON ومطابقة أنماط الصفوف Row Pattern Matching.
- SQL:2023: أضاف النوع JSON نفسه، وجزءاً جديداً للاستعلام عن البيانات كرسم بياني Property Graph Queries واختصاراً SQL/PGQ، كما يلخص [Peter Eisentraut في مقاله عن ما الجديد في SQL:2023](https://peter.eisentraut.org/blog/2023/04/04/sql-2023-is-finished-here-is-whats-new?ref=arabroot.io).

والاسم الرسمي للمعيار اليوم هو ISO/IEC 9075، وآخر إصدار له هو SQL:2023 كما يذكر [توثيق PostgreSQL عن التوافق مع المعيار](https://www.postgresql.org/docs/18/features.html?ref=arabroot.io)، ويقول نفس التوثيق إن كل إصدار يلغي الذي قبله. والصورة التالية تجمع هذا التاريخ:

![خطان زمنيان. الأول للغة والمنتجات الأولى: 1970 ورقة Codd عن النموذج العلائقي في IBM، 1973 بداية مشروع System R، 1974 لغة SEQUEL من Chamberlin وBoyce، ثم تغيير الاسم من SEQUEL إلى SQL بسبب علامة تجارية، 1979 Oracle أول منتج تجاري، 1983 صدور IBM DB2. والثاني لإصدارات المعيار ISO/IEC 9075: عام 1986 و1987 أول معيار من ANSI ثم ISO، ثم SQL-92 مراجعة كبيرة، وSQL:1999 الاستعلامات التكرارية والقوادح، وSQL:2003 دوال النوافذ وأعمدة الهوية، وSQL:2016 دوال JSON ومطابقة الأنماط، وSQL:2023 النوع JSON والاستعلام عن الرسم البياني PGQ](https://arabroot.io/content/images/2026/10/sql-basics-02-history.webp)

اللغة بدأت في مختبر IBM، ثم أصبحت معياراً دولياً يتجدد كل بضع سنوات

### المحركات المشهورة ولهجاتها

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

| المحرك               | اللهجة واللغة الإجرائية      | ملاحظة                                                          |
| -------------------- | ---------------------------- | --------------------------------------------------------------- |
| Oracle Database      | Oracle SQL وPL/SQL           | أول تطبيق تجاري للغة عام 1979، ويعامل النص الفارغ كأنه NULL     |
| IBM Db2              | SQL PL                       | صدر عام 1983 وهو من نتائج مشروع System R نفسه                   |
| Microsoft SQL Server | Transact-SQL واختصاراً T-SQL | يستخدم TOP بدل LIMIT، وOUTPUT بدل RETURNING                     |
| MySQL وMariaDB       | MySQL SQL                    | مقارنة النصوص فيه لا تفرق بين الحروف الكبيرة والصغيرة افتراضياً |
| PostgreSQL           | PostgreSQL SQL وPL/pgSQL     | المحرك الذي نستخدمه في هذه السلسلة، وهو قريب جداً من المعيار    |
| SQLite               | SQLite SQL                   | مكتبة تعمل داخل التطبيق نفسه بلا سيرفر منفصل                    |

وتواريخ PostgreSQL من [صفحة تاريخه في التوثيق](https://www.postgresql.org/docs/18/history.html?ref=arabroot.io)، فقد بدأ تطبيق POSTGRES عام 1986، ثم أضاف إليه Andrew Yu وJolly Chen مفسراً للغة SQL عام 1994 باسم Postgres95، ثم تغير الاسم إلى PostgreSQL عام 1996\. وتاريخ SQLite من [سجل إصداراته](https://www.sqlite.org/chronology.html?ref=arabroot.io) الذي يبدأ بالإصدار 1.0 في أغسطس 2000\. وأما معاملة النص الفارغ كأنه NULL في Oracle فيذكرها [توثيق Oracle عن NULL](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Nulls.html?ref=arabroot.io) صراحة.

والخبر الجيد أن ما سوف تتعلمه في هذه السلسلة يعمل في كل هذه المحركات تقريباً كما هو، والسبب أن الأساسيات من المعيار نفسه، وسوف نستخدم PostgreSQL 18 كمحرك أساسي، وحيث يختلف المحرك عن غيره سوف نذكر الفرق، وقد شغلنا هذه المواضع بأنفسنا على MySQL 8.4 وSQL Server 2022 وSQLite 3.49 حتى نتأكد منها.

## خمس مجموعات من الأوامر

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

| المجموعة                                   | ما تعمل عليه                               | الأوامر                            | مثال                                                |
| ------------------------------------------ | ------------------------------------------ | ---------------------------------- | --------------------------------------------------- |
| DDL واختصارها Data Definition Language     | هيكل البيانات: الجداول والأعمدة والقيود    | CREATE وALTER وDROP وTRUNCATE      | CREATE TABLE notes (id int PRIMARY KEY, body text); |
| DML واختصارها Data Manipulation Language   | الصفوف نفسها: إضافة وتعديل وحذف            | INSERT وUPDATE وDELETE وMERGE      | UPDATE courses SET price = 320 WHERE id = 1;        |
| DQL واختصارها Data Query Language          | قراءة البيانات دون تغييرها                 | SELECT                             | SELECT title, price FROM courses;                   |
| DCL واختصارها Data Control Language        | الصلاحيات: من يستطيع أن يفعل ماذا          | GRANT وREVOKE                      | GRANT SELECT ON courses TO report\_reader;          |
| TCL واختصارها Transaction Control Language | المعاملة: مجموعة أوامر تنفذ كلها أو لا شيء | BEGIN وCOMMIT وROLLBACK وSAVEPOINT | BEGIN; … ROLLBACK;                                  |

والصورة التالية تبين المجموعات الخمس:

![خمس بطاقات ملونة: DDL أي Data Definition وفيها CREATE وALTER وDROP وTRUNCATE وتعمل على الهيكل أي الجداول والأعمدة والمفاتيح، وDML أي Data Manipulation وفيها INSERT وUPDATE وDELETE وMERGE وتعمل على الصفوف، وDQL أي Data Query وفيها SELECT للقراءة، وDCL أي Data Control وفيها GRANT وREVOKE للصلاحيات، وTCL أي Transaction Control وفيها BEGIN وCOMMIT وROLLBACK وSAVEPOINT للمعاملة](https://arabroot.io/content/images/2026/10/sql-basics-03-commands.webp)

المجموعة تخبرك بما يعمل عليه الأمر، وبالتالي بحجم الضرر إذا أخطأت فيه

يجدر الإشارة هنا أن بعض المراجع تضع SELECT ضمن DML ولا تفصل DQL، وهذا اختلاف في التسمية فقط، والمهم أن تعرف ما يعمل عليه كل أمر. وسوف ترى كل مجموعة منها تعمل على قاعدة بياناتنا في الأقسام التالية، بنفس الترتيب الذي تحتاجها به: ننشئ الجداول بأوامر DDL، ثم نضيف البيانات ونعدلها بأوامر DML داخل معاملات TCL، ثم نعطي مستخدماً صلاحية القراءة بأوامر DCL، ثم نقضي بقية الجزء في SELECT.

## تجهيز المختبر Lab

سوف نشغل PostgreSQL 18 في Container يستمع على العنوان المحلي فقط، والسبب أن قاعدة بيانات بباسورد بسيط مثل `lab` يجب ألا تكون مفتوحة على الشبكة، وإذا لم يكن Docker مثبتاً لديك فقد شرحنا تثبيته في [تثبيت Docker على Ubuntu](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/):

```bash
docker run -d --name lab-sql-pg -e POSTGRES_PASSWORD=lab -e POSTGRES_DB=academy \
  -p 127.0.0.1:21471:5432 postgres:18.6

```

ثم ادخل إلى psql، وهو الكلاينت النصي الرسمي لـ PostgreSQL:

```bash
docker exec -it lab-sql-pg psql -U postgres -d academy

```

وأول أمر داخل psql هو التالي، والسبب أن psql يعرض القيمة NULL كخانة فارغة لا تفرقها عن النص الفارغ، وهذا الأمر من [أوامر psql](https://www.postgresql.org/docs/18/app-psql.html?ref=arabroot.io) يجعلها تظهر بالاسم، وكل المخرجات في هذه السلسلة مأخوذة بهذا الإعداد:

```sql
\pset null NULL

```

والأوامر التي تبدأ بشرطة مائلة عكسية مثل `\pset` و`\dt` هي أوامر psql نفسه وليست من SQL، لذلك لا تنتهي بفاصلة منقوطة ولا تعمل في برنامج آخر. أما جمل SQL فتنتهي دائماً بفاصلة منقوطة `;`، وإذا ضغطت Enter دون أن تكتبها فسوف ينتظر psql بقية الجملة ويتغير شكل المحث Prompt، وهذا أول ما يربك المبتدئ.

## أنواع البيانات Data Types

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

| الاستخدام                    | PostgreSQL       | MySQL                             | SQL Server            | SQLite                  |
| ---------------------------- | ---------------- | --------------------------------- | --------------------- | ----------------------- |
| عدد صحيح                     | int وbigint      | INT وBIGINT                       | int وbigint           | INTEGER                 |
| مبلغ مالي بخانات عشرية ثابتة | numeric(8,2)     | DECIMAL(8,2)                      | decimal(8,2)          | NUMERIC                 |
| رقم عشري تقريبي للقياسات     | double precision | DOUBLE                            | float                 | REAL                    |
| نص                           | text وvarchar(n) | VARCHAR(n) وTEXT                  | nvarchar(n)           | TEXT                    |
| صح أو خطأ                    | boolean          | BOOLEAN وهو اسم آخر لـ TINYINT(1) | bit                   | INTEGER بالقيمتين 0 و1  |
| تاريخ                        | date             | DATE                              | date                  | TEXT أو REAL أو INTEGER |
| لحظة زمنية                   | timestamptz      | DATETIME وTIMESTAMP               | datetimeoffset        | TEXT أو REAL أو INTEGER |
| مستند JSON                   | jsonb            | JSON                              | nvarchar مع دوال JSON | TEXT مع دوال JSON       |

والمصادر هنا هي صفحات الأنواع في [PostgreSQL](https://www.postgresql.org/docs/18/datatype.html?ref=arabroot.io) و[MySQL](https://dev.mysql.com/doc/refman/8.4/en/data-types.html?ref=arabroot.io) و[SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql?ref=arabroot.io) و[SQLite](https://www.sqlite.org/datatype3.html?ref=arabroot.io)، ولاحظ أن SQLite مختلف عن البقية، فالنوع فيه يخص القيمة وليس العمود، ولا يوجد فيه نوع منفصل للتاريخ ولا للقيمة المنطقية. ولنرى الآن لماذا يهم النوع، فالمثال التالي يقسم 7 على 2 بطريقتين:

```sql
SELECT 7 / 2 AS int_div, 7 / 2.0 AS num_div, 7 % 2 AS remainder;

```

والمخرج سوف يكون كما يلي:

```text
 int_div |      num_div       | remainder
---------+--------------------+-----------
       3 | 3.5000000000000000 |         1
(1 row)

```

قسمة عدد صحيح على عدد صحيح تعطي عدداً صحيحاً وترمي الكسر، فالنتيجة 3 وليست 3.5، وهذا يحدث في PostgreSQL وSQL Server وSQLite، وقد شغلنا نفس الجملة عليها فأعطت 3، أما MySQL فأعطى 3.5000، لذلك إذا كنت تحسب نسبة مئوية من عمودين صحيحين فاجعل أحدهما عشرياً. والمثال الثاني يقارن نوع المال numeric بالنوع التقريبي:

```sql
SELECT 0.1 + 0.2 = 0.3 AS numeric_ok,
       0.1::float8 + 0.2::float8 = 0.3::float8 AS float_ok;

```

```text
 numeric_ok | float_ok
------------+----------
 t          | f
(1 row)

```

النوع numeric يحفظ الرقم العشري كما هو، أما float8 وهو اسم آخر لـ double precision فيحفظ أقرب رقم ثنائي، فلا تساوي 0.1 + 0.2 القيمة 0.3 بالضبط، وقد شرحنا السبب بالتفصيل في [الأرقام العشرية Floating Point من الداخل](https://arabroot.io/articles/%D8%A7%D9%84%D8%A3%D8%B1%D9%82%D8%A7%D9%85-%D8%A7%D9%84%D8%B9%D8%B4%D8%B1%D9%8A%D8%A9-floating-point-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)، والقاعدة: المال دائماً numeric. وإذا أردت أن تعرف نوع أي قيمة فاسأل قاعدة البيانات بالدالة `pg_typeof`:

```sql
SELECT pg_typeof(42) AS a, pg_typeof(42.5) AS b, pg_typeof('Sara'::text) AS c,
       pg_typeof(current_date) AS d, pg_typeof(now()) AS e;

```

```text
    a    |    b    |  c   |  d   |            e
---------+---------+------+------+--------------------------
 integer | numeric | text | date | timestamp with time zone
(1 row)

```

والعلامة `::` هي طريقة PostgreSQL المختصرة لتحويل النوع Cast، وصيغة المعيار التي تعمل في كل المحركات هي `CAST('Sara' AS text)`. والآن لنكسر الأنواع عن قصد، فنعطيها تاريخاً غير موجود وقيمة منطقية غير مفهومة:

```sql
SELECT '2026-02-30'::date;
SELECT 'abc'::boolean;

```

```text
ERROR:  date/time field value out of range: "2026-02-30"
LINE 1: SELECT '2026-02-30'::date;
               ^
ERROR:  invalid input syntax for type boolean: "abc"
LINE 1: SELECT 'abc'::boolean;
               ^

```

وهذا بالضبط ما تريده: لو كان العمود نصاً لقبل «30 فبراير» بصمت، ولاكتشفت الخطأ عندما يفشل تقرير الشهر. وأخيراً النص المحدود الطول، فالنوع `varchar(3)` يرفض نصاً أطول من ثلاثة حروف عند الإضافة:

```sql
CREATE TABLE t_demo (code varchar(3));
INSERT INTO t_demo VALUES ('Riyadh');
DROP TABLE t_demo;

```

```text
CREATE TABLE
ERROR:  value too long for type character varying(3)
DROP TABLE

```

وكذلك يرفضه MySQL 8.4 بالخطأ `Data too long for column` لأن الوضع الصارم Strict Mode مفعل فيه افتراضياً، أما SQLite فقد قبل النص كاملاً `Riyadh` في عمود `varchar(3)` دون أي خطأ، لأنه لا يطبق الطول أصلاً، فلا تعتمد عليه في التحقق Validation. وفي PostgreSQL يفضل أغلب المطورين النوع `text` ويضعون الحد الأقصى في قيد CHECK إن احتاجوه، والسبب أن [توثيق أنواع النصوص](https://www.postgresql.org/docs/18/datatype-character.html?ref=arabroot.io) يقول إنه لا يوجد فرق في الأداء بين الأنواع الثلاثة.

## إنشاء الجداول CREATE TABLE

الأمر [CREATE TABLE](https://www.postgresql.org/docs/18/sql-createtable.html?ref=arabroot.io) ينشئ جدولاً، وتكتب فيه اسم كل عمود ونوعه والقيود عليه. واحفظ الجداول الخمسة في ملف باسم `schema.sql`، فهذا الملف هو الذي سوف نستخدمه في الأجزاء الثلاثة:

```sql
CREATE TABLE instructors (
    id          int   GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name        text  NOT NULL,
    manager_id  int   REFERENCES instructors(id)
);

CREATE TABLE students (
    id         int   GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name       text  NOT NULL,
    city       text  NOT NULL,
    email      text  NOT NULL UNIQUE,
    phone      text,
    joined_on  date  NOT NULL DEFAULT current_date
);

CREATE TABLE courses (
    id             int           GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title          text          NOT NULL,
    track          text          NOT NULL,
    price          numeric(8,2)  NOT NULL CHECK (price >= 0),
    capacity       int           NOT NULL CHECK (capacity > 0),
    instructor_id  int           REFERENCES instructors(id) ON DELETE SET NULL
);

CREATE TABLE enrollments (
    id           int   GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    student_id   int   NOT NULL REFERENCES students(id),
    course_id    int   NOT NULL REFERENCES courses(id),
    enrolled_on  date  NOT NULL DEFAULT current_date,
    grade        int   CHECK (grade BETWEEN 0 AND 100),
    UNIQUE (student_id, course_id)
);

CREATE TABLE payments (
    id             int           GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    enrollment_id  int           NOT NULL REFERENCES enrollments(id) ON DELETE CASCADE,
    amount         numeric(8,2)  NOT NULL CHECK (amount > 0),
    paid_on        date          NOT NULL
);

```

ثم نفذ الملف من الطرفية Terminal، خارج psql:

```bash
docker exec -i lab-sql-pg psql -U postgres -d academy < schema.sql

```

والمخرج سوف يكون كما يلي:

```text
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE

```

في الإعداد أعلاه لاحظ التالي:

- المفتاح الأساسي `id` في كل جدول تولده قاعدة البيانات بالصيغة [GENERATED ALWAYS AS IDENTITY](https://www.postgresql.org/docs/18/ddl-identity-columns.html?ref=arabroot.io)، وهي صيغة المعيار منذ SQL:2003، ويقابلها `AUTO_INCREMENT` في MySQL و`IDENTITY(1,1)` في SQL Server و`INTEGER PRIMARY KEY` في SQLite.
- الكلمة `REFERENCES` تعرف المفتاح الأجنبي، والجدول instructors يشير إلى نفسه عبر `manager_id` لأن لكل مدرب مديراً من نفس الجدول، إلا رئيسة المعهد فمديرها NULL.
- ترتيب الجداول في الملف مهم، فلا يمكن أن تنشئ courses قبل instructors لأن المفتاح الأجنبي يشير إلى جدول يجب أن يكون موجوداً.
- الكلمة `DEFAULT current_date` تعني أن قاعدة البيانات تضع تاريخ اليوم إذا لم تعطها قيمة، وسوف تراها تعمل في قسم الإضافة.
- القيود NOT NULL وUNIQUE وCHECK وON DELETE نشرحها في [قسم القيود](#constraints) بعد أن نضيف البيانات، لأننا نحتاج بيانات حتى نرى كل قيد يرفض شيئاً، وكذلك تعديل الجداول وحذفها في [قسم ALTER وDROP](#alter-drop).

والآن ارجع إلى psql واطلب قائمة الجداول بالأمر `\dt` ثم وصف جدول الدورات بالأمر `\d courses`:

```text
             List of tables
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+----------
 public | courses     | table | postgres
 public | enrollments | table | postgres
 public | instructors | table | postgres
 public | payments    | table | postgres
 public | students    | table | postgres
(5 rows)

                               Table "public.courses"
    Column     |     Type     | Collation | Nullable |           Default
---------------+--------------+-----------+----------+------------------------------
 id            | integer      |           | not null | generated always as identity
 title         | text         |           | not null |
 track         | text         |           | not null |
 price         | numeric(8,2) |           | not null |
 capacity      | integer      |           | not null |
 instructor_id | integer      |           |          |
Indexes:
    "courses_pkey" PRIMARY KEY, btree (id)
Check constraints:
    "courses_capacity_check" CHECK (capacity > 0)
    "courses_price_check" CHECK (price >= 0::numeric)
Foreign-key constraints:
    "courses_instructor_id_fkey" FOREIGN KEY (instructor_id) REFERENCES instructors(id) ON DELETE SET NULL
Referenced by:
    TABLE "enrollments" CONSTRAINT "enrollments_course_id_fkey" FOREIGN KEY (course_id) REFERENCES courses(id)

```

لاحظ أن PostgreSQL أعطى كل قيد اسماً مثل `courses_price_check`، وهذا الاسم هو الذي سوف تراه في رسائل الخطأ، فإذا كنت تريد رسائل أوضح فاكتب للقيد اسماً بنفسك بالصيغة `CONSTRAINT price_not_negative CHECK (price >= 0)`.

## إضافة البيانات INSERT

الأمر [INSERT](https://www.postgresql.org/docs/18/dml-insert.html?ref=arabroot.io) يضيف صفاً أو أكثر، وتكتب بعد اسم الجدول الأعمدة التي ستعطيها قيماً، ثم القيم بنفس الترتيب بعد الكلمة VALUES، ويمكنك أن تضيف صفوفاً كثيرة في جملة واحدة بفصلها بفاصلة، وهذا أسرع بكثير من جملة لكل صف لأن قاعدة البيانات تستقبل طلباً واحداً. واحفظ البيانات التالية في ملف باسم `seed.sql`:

```sql
INSERT INTO instructors (name, manager_id) VALUES
 ('Nora',   NULL),
 ('Majed',  1),
 ('Rania',  1),
 ('Tariq',  2),
 ('Yasser', 2),
 ('Dana',   3);

INSERT INTO students (name, city, email, phone, joined_on) VALUES
 ('Sara',   'Riyadh', 'sara@example.com',   '0500000001', '2026-08-20'),
 ('Omar',   'Jeddah', 'omar@example.com',   NULL,         '2026-08-25'),
 ('Lina',   'Riyadh', 'lina@example.com',   '0500000003', '2026-09-01'),
 ('Khalid', 'Dammam', 'khalid@example.com', NULL,         '2026-09-02'),
 ('Huda',   'Jeddah', 'huda@example.com',   '0500000005', '2026-09-03'),
 ('Faisal', 'Abha',   'faisal@example.com', '0500000006', '2026-09-15');

INSERT INTO courses (title, track, price, capacity, instructor_id) VALUES
 ('Linux Basics', 'ops',  300, 20, 4),
 ('PostgreSQL',   'data', 450, 15, 3),
 ('Docker',       'ops',  400,  3, 5),
 ('Python',       'dev',  350, 25, 6),
 ('Kubernetes',   'ops',  600, 10, NULL);

INSERT INTO enrollments (student_id, course_id, enrolled_on, grade) VALUES
 (1, 1, '2026-09-01', 90),
 (1, 2, '2026-09-03', 85),
 (2, 1, '2026-09-02', 70),
 (2, 3, '2026-09-10', NULL),
 (3, 2, '2026-09-05', 95),
 (3, 4, '2026-09-06', NULL),
 (4, 4, '2026-09-07', 60),
 (5, 2, '2026-09-08', NULL),
 (5, 3, '2026-09-12', 88);

INSERT INTO payments (enrollment_id, amount, paid_on) VALUES
 (1, 300, '2026-09-01'),
 (2, 225, '2026-09-03'),
 (2, 225, '2026-10-03'),
 (3, 300, '2026-09-02'),
 (4, 200, '2026-09-10'),
 (5, 450, '2026-09-05'),
 (7, 350, '2026-09-07'),
 (8, 450, '2026-09-08'),
 (9, 200, '2026-09-12'),
 (9, 200, '2026-10-01');

```

ثم نفذه من الطرفية:

```bash
docker exec -i lab-sql-pg psql -U postgres -d academy < seed.sql

```

```text
INSERT 0 6
INSERT 0 6
INSERT 0 5
INSERT 0 9
INSERT 0 10

```

في البيانات أعلاه لاحظ التالي:

- الرقم الأخير في `INSERT 0 6` هو عدد الصفوف التي أضيفت، أما الصفر فهو من بقايا إصدارات قديمة ويبقى صفراً دائماً.
- لم نكتب قيمة للعمود `id`، لأن قاعدة البيانات تولده بالترتيب 1 ثم 2 ثم 3، ولهذا نستطيع أن نكتب `(1, 2, ...)` في التسجيلات ونحن نعرف أن الطالب 1 هو سارة والدورة 2 هي PostgreSQL.
- النص بين علامتي تنصيص مفردة `'Riyadh'`، والتاريخ نص بالصيغة `'2026-09-01'` وهي صيغة ISO 8601 التي تفهمها كل المحركات، والأرقام بلا علامات تنصيص.
- عمر وخالد بلا رقم جوال، وثلاثة تسجيلات بلا درجة لأن أصحابها لم ينهوا الدورة، ودورة Kubernetes بلا مدرب بعد، وفيصل لم يسجل في أي دورة، وهذه الفراغات مقصودة لأنها مصدر أغلب الأخطاء التي سوف نراها.
- سارة دفعت دورة PostgreSQL على دفعتين، وهدى دفعت Docker على دفعتين، وعمر دفع نصف Docker، ولينا لم تدفع Python بعد، وهذا ما سوف نحتاجه في الجزء الثاني.

## تعديل الجداول وحذفها ALTER وDROP

بعد أن أصبحت في الجداول بيانات، لنرى كيف نعدل جدولاً موجوداً، وهذا بالأمر [ALTER TABLE](https://www.postgresql.org/docs/18/sql-altertable.html?ref=arabroot.io)، فالمثال التالي يضيف عموداً لسنة الميلاد ثم يحذفه:

```sql
ALTER TABLE students ADD COLUMN birth_year int;
SELECT id, name, birth_year FROM students WHERE id <= 2;
ALTER TABLE students DROP COLUMN birth_year;

```

والمخرج سوف يكون كما يلي:

```text
ALTER TABLE
 id | name | birth_year
----+------+------------
  1 | Sara |       NULL
  2 | Omar |       NULL
(2 rows)

ALTER TABLE

```

العمود الجديد ظهر في كل الصفوف الموجودة بالقيمة NULL، لأن قاعدة البيانات لا تعرف سنة ميلاد أحد. وأما حذف جدول كامل فهو بالأمر DROP TABLE، وإذا حاولت حذف جدول غير موجود فسوف تحصل على خطأ، إلا إذا أضفت `IF EXISTS`:

```sql
CREATE TABLE notes (id int PRIMARY KEY, body text);
DROP TABLE notes;
DROP TABLE notes;
DROP TABLE IF EXISTS notes;

```

```text
CREATE TABLE
DROP TABLE
ERROR:  table "notes" does not exist
NOTICE:  table "notes" does not exist, skipping
DROP TABLE

```

والصيغة `IF EXISTS` مفيدة في سكربتات الإعداد التي تشغلها أكثر من مرة. وهنا ميزة في PostgreSQL قد لا تتوقعها، وهي أن أوامر DDL نفسها تعمل داخل المعاملة Transaction، فإذا حذفت جدولاً بالخطأ داخل BEGIN فإن ROLLBACK يعيده بكل بياناته:

```sql
BEGIN;
DROP TABLE payments;
SELECT count(*) FROM payments;
ROLLBACK;
SELECT count(*) FROM payments;

```

```text
BEGIN
DROP TABLE
ERROR:  relation "payments" does not exist
LINE 1: SELECT count(*) FROM payments;
                             ^
ROLLBACK
 count
-------
    10
(1 row)

```

وهذا لا يعمل في كل المحركات، فقد شغلنا نفس الفكرة على MySQL 8.4 فحذف `DROP TABLE` الجدول نهائياً ولم يعده ROLLBACK، والسبب أن MySQL ينهي المعاملة تلقائياً قبل أوامر DDL، كما يذكر [توثيق التأكيد الضمني Implicit Commit](https://dev.mysql.com/doc/refman/8.4/en/implicit-commit.html?ref=arabroot.io). أما SQL Server 2022 وSQLite فأعادا الجدول بعد ROLLBACK مثل PostgreSQL. لذلك إذا كنت تعمل على MySQL فخذ نسخة احتياطية قبل أي تعديل في الهيكل على بيانات حقيقية.

## القيود Constraints تحمي بياناتك

القيد Constraint قاعدة تكتبها مرة واحدة في تعريف الجدول، وقاعدة البيانات تطبقها على كل INSERT وUPDATE وDELETE من أي تطبيق أو شخص، لذلك هي آخر خط دفاع عن بياناتك حتى لو نسي مطور التحقق في الكود، وقد شرحنا لماذا تحمي قاعدة البيانات نفسها بالقيود في جزء [القيود Constraints](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/#constraints) من مقال التصميم، والآن سوف نرى كل قيد منها يرفض بيانات خاطئة، والمرجع هو [توثيق القيود في PostgreSQL](https://www.postgresql.org/docs/18/ddl-constraints.html?ref=arabroot.io). ونبدأ بالقيد NOT NULL، فالمدينة إلزامية:

```sql
INSERT INTO students (name, city, email) VALUES ('Reem', NULL, 'reem@example.com');

```

```text
ERROR:  null value in column "city" of relation "students" violates not-null constraint
DETAIL:  Failing row contains (7, Reem, null, reem@example.com, null, 2026-10-10).

```

ثم UNIQUE، فالبريد لا يتكرر:

```sql
INSERT INTO students (name, city, email) VALUES ('Reem', 'Riyadh', 'sara@example.com');

```

```text
ERROR:  duplicate key value violates unique constraint "students_email_key"
DETAIL:  Key (email)=(sara@example.com) already exists.

```

ثم CHECK، فالسعر لا يكون سالباً، ولاحظ أن القيد يعمل على UPDATE كما يعمل على INSERT:

```sql
UPDATE courses SET price = -50 WHERE id = 1;

```

```text
ERROR:  new row for relation "courses" violates check constraint "courses_price_check"
DETAIL:  Failing row contains (1, Linux Basics, ops, -50.00, 20, 4).

```

ثم المفتاح الأجنبي، فلا يوجد تسجيل في دورة غير موجودة، ثم القيد UNIQUE على عمودين معاً، فالطالب لا يسجل في نفس الدورة مرتين:

```sql
INSERT INTO enrollments (student_id, course_id) VALUES (1, 99);
INSERT INTO enrollments (student_id, course_id) VALUES (1, 1);

```

```text
ERROR:  insert or update on table "enrollments" violates foreign key constraint "enrollments_course_id_fkey"
DETAIL:  Key (course_id)=(99) is not present in table "courses".
ERROR:  duplicate key value violates unique constraint "enrollments_student_id_course_id_key"
DETAIL:  Key (student_id, course_id)=(1, 1) already exists.

```

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

```sql
DELETE FROM students WHERE id = 1;

```

```text
ERROR:  update or delete on table "students" violates foreign key constraint "enrollments_student_id_fkey" on table "enrollments"
DETAIL:  Key (id)=(1) is still referenced from table "enrollments".

```

والخيار الثاني `ON DELETE SET NULL` كتبناه للمدرب في جدول الدورات، فإذا ترك المدرب المعهد بقيت دورته بلا مدرب. والخيار الثالث `ON DELETE CASCADE` كتبناه للدفعات، فإذا حذف تسجيل حذفت دفعاته معه. وسوف نجرب الخيارين داخل معاملة ونلغيها بعد ذلك حتى تبقى البيانات كما هي:

```sql
BEGIN;
DELETE FROM instructors WHERE id = 5;
SELECT title, instructor_id FROM courses WHERE id = 3;
ROLLBACK;

BEGIN;
SELECT count(*) FROM payments WHERE enrollment_id = 9;
DELETE FROM enrollments WHERE id = 9;
SELECT count(*) FROM payments WHERE enrollment_id = 9;
ROLLBACK;

```

```text
BEGIN
DELETE 1
 title  | instructor_id
--------+---------------
 Docker |          NULL
(1 row)

ROLLBACK
BEGIN
 count
-------
     2
(1 row)

DELETE 1
 count
-------
     0
(1 row)

ROLLBACK

```

في المخرج أعلاه لاحظ التالي:

- حذف المدرب Yasser نجح، وأصبح مدرب دورة Docker هو NULL بدل أن يرفض الحذف.
- حذف التسجيل رقم 9 حذف معه دفعتي هدى دون أن نكتب DELETE على جدول الدفعات، والعبارة `DELETE 1` تعد الصفوف التي حذفتها الجملة نفسها فقط، لذلك لا تخبرك بالدفعات التي حذفت معها، وهذا هو خطر CASCADE: حذف واحد قد يمسح آلاف الصفوف في جداول أخرى دون أن تراها.
- القاعدة العملية أن تستخدم الرفض الافتراضي للبيانات المهمة مثل الطلاب والفواتير، وCASCADE للبيانات التي لا معنى لها بدون صاحبها، وSET NULL عندما يبقى الصف مفيداً بعد اختفاء ما يشير إليه.

وفي MySQL لم تكن قيود CHECK تطبق إلا من الإصدار 8.0.16 كما يذكر [توثيق قيود CHECK في MySQL](https://dev.mysql.com/doc/refman/8.4/en/create-table-check-constraints.html?ref=arabroot.io)، وقد رفض MySQL 8.4 السعر السالب برسالة `Check constraint 'c_chk_1' is violated`، وكذلك رفضه SQLite 3.49 برسالة `CHECK constraint failed`، لذلك إذا كنت تعمل على نسخة قديمة من MySQL فلا تفترض أن القيد يعمل لأنه مكتوب.

## التعديل والحذف بأمان

بعد أن رأيت الإضافة الأساسية، هناك إضافة مفيدة في PostgreSQL هي [RETURNING](https://www.postgresql.org/docs/18/dml-returning.html?ref=arabroot.io)، وهي ترجع لك ما كتبته قاعدة البيانات فعلاً، مثل الرقم الذي ولدته والقيمة الافتراضية، دون استعلام ثان:

```sql
INSERT INTO students (name, city, email)
VALUES ('Reem', 'Riyadh', 'reem@example.com')
RETURNING id, name, phone, joined_on;

```

```text
 id | name | phone | joined_on
----+------+-------+------------
  9 | Reem | NULL  | 2026-10-10
(1 row)

INSERT 0 1

```

وقد يتساءل البعض: لدينا ستة طلاب، فلماذا أخذت ريم الرقم 9 وليس 7؟ والإجابة في قسم القيود، فمحاولتا إضافة ريم اللتان فشلتا بسبب NOT NULL وUNIQUE أخذتا الرقمين 7 و8 من المولد ثم فشلتا، والمولد لا يرجع الأرقام التي أخذها، والسبب كما يذكر [توثيق المولدات Sequences](https://www.postgresql.org/docs/18/functions-sequence.html?ref=arabroot.io) أن إرجاعها يجعل الإضافات المتزامنة تنتظر بعضها. لذلك لا تكتب كوداً يفترض أن الأرقام متتالية بلا فراغات، والعمود `joined_on` أخذ تاريخ يوم التشغيل لأننا لم نعطه قيمة. ويقابل RETURNING في SQL Server الكلمة [OUTPUT](https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql?ref=arabroot.io)، ويدعمها SQLite منذ الإصدار 3.35 كما يذكر [توثيق SQLite](https://www.sqlite.org/lang%5Freturning.html?ref=arabroot.io)، أما MySQL فلا يدعمها، ويستخدم المطور فيه الدالة `LAST_INSERT_ID()` بعد الإضافة. والمثال التالي يضيف طالبين في جملة واحدة، ثم يحاول أن يفرض رقماً بنفسه:

```sql
INSERT INTO students (name, city, email, phone) VALUES
 ('Yousef', 'Makkah', 'yousef@example.com', '0500000010'),
 ('Maha',   'Riyadh', 'maha@example.com',   NULL)
RETURNING id, name;

INSERT INTO students (id, name, city, email) VALUES (100, 'Test', 'Riyadh', 'test@example.com');

```

```text
 id |  name
----+--------
 10 | Yousef
 11 | Maha
(2 rows)

INSERT 0 2
ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.

```

والكلمة ALWAYS في تعريف العمود هي التي منعت الرقم 100، وهذا مقصود حتى لا يصطدم رقم كتبه شخص بيده برقم سوف يولده المولد لاحقاً.

أما UPDATE وDELETE فهما أخطر جملتين في SQL، والسبب أن نسيان WHERE لا يعطي خطأ وإنما ينفذ الجملة على الجدول كله. لذلك القاعدة التي ننصح بها ثلاث خطوات: شغل الشرط أولاً في SELECT count(\*) واعرف كم صفاً تتوقع، ثم نفذ الجملة داخل معاملة تبدأ بـ BEGIN، ثم قارن عدد الصفوف الذي تطبعه قاعدة البيانات بما توقعته قبل أن تكتب COMMIT. والمثال التالي يخفض أسعار دورات العمليات ops بنسبة 10%:

```sql
SELECT count(*) FROM courses WHERE track = 'ops';

BEGIN;
UPDATE courses SET price = price * 0.9
WHERE track = 'ops'
RETURNING title, price;
ROLLBACK;

```

```text
 count
-------
     3
(1 row)

BEGIN
    title     | price
--------------+--------
 Linux Basics | 270.00
 Docker       | 360.00
 Kubernetes   | 540.00
(3 rows)

UPDATE 3
ROLLBACK

```

توقعنا ثلاث دورات فوجدنا `UPDATE 3`، ورأينا الأسعار الجديدة بفضل RETURNING قبل أن نقرر، ولو كان الأمر حقيقياً لكتبنا COMMIT بدل ROLLBACK. والآن لنكسر المثال عن قصد وننسى الشرط:

```sql
BEGIN;
UPDATE courses SET price = 0;
ROLLBACK;

```

```text
BEGIN
UPDATE 5
ROLLBACK

```

الرقم 5 بدل 3 هو جرس الإنذار، ولأننا داخل معاملة فالأمر ROLLBACK أعاد كل الأسعار كما كانت. ولو نفذت نفس الجملة خارج معاملة في psql لأصبحت كل الدورات مجانية فوراً، والسبب أن psql يؤكد كل جملة وحدها افتراضياً وهذا ما يسمى Autocommit، ولا رجوع عنها إلا من نسخة احتياطية.

### المعاملة BEGIN وCOMMIT وROLLBACK

المعاملة Transaction التي استخدمناها في الأمثلة السابقة هي مجموعة جمل تبدأ بـ BEGIN، ولا يراها أحد غيرك ولا تصبح دائمة حتى تكتب COMMIT، وإذا كتبت ROLLBACK ألغيت كلها كأنها لم تحدث، وهذا هو سبب نصيحتنا بتنفيذ UPDATE وDELETE داخلها. والمعاملة لها أداة صغيرة مفيدة هي نقطة الحفظ SAVEPOINT، وهي تسمح لك أن تلغي جزءاً من المعاملة وتبقي الباقي، مثل نقطة الحفظ في الألعاب:

```sql
BEGIN;
UPDATE courses SET price = price + 50 WHERE id = 1;
SAVEPOINT before_delete;
DELETE FROM payments WHERE id = 1;
ROLLBACK TO SAVEPOINT before_delete;
SELECT (SELECT price FROM courses WHERE id = 1) AS linux_price,
       (SELECT count(*) FROM payments)           AS payments;
ROLLBACK;

```

```text
BEGIN
UPDATE 1
SAVEPOINT
DELETE 1
ROLLBACK
 linux_price | payments
-------------+----------
      350.00 |       10
(1 row)

ROLLBACK

```

الحذف ألغي لأنه بعد نقطة الحفظ، فبقيت الدفعات عشراً، أما زيادة السعر فبقيت 350 لأنها قبلها، ثم ألغى ROLLBACK الأخير كل شيء. ووعود المعاملة الأربعة التي تختصر بكلمة ACID، وكيف يلغي PostgreSQL المعاملة كلها من أول خطأ، شرحناها مع أمثلتها في [الجزء الثالث](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/#transactions).

### وماذا لو عملت جلستان على نفس الصفوف؟

كل أمثلة هذا الجزء جلسة واحدة، أما التطبيق الحقيقي فيخدم مئات المستخدمين في نفس اللحظة، فلنفرض أن طالبين طلبا آخر مقعد في دورة Docker في نفس الثانية، وكل طلب قرأ عدد المسجلين فوجد مقعداً فارغاً ثم سجل، فأصبح في الدورة طالب زائد دون أي رسالة خطأ. وما تراه كل معاملة من تغييرات المعاملات الأخرى يحدده مستوى العزل Isolation Level، وقد شغلنا هذه الحالة بالضبط على جلستين حقيقيتين في [مستويات العزل في الجزء الثالث](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/#isolation)، ورأينا متى يمنعها PostgreSQL ومتى لا يمنعها.

### الحذف DELETE

وآخر شيء هنا هو الحذف، فالمثال التالي يحذف الطلاب الثلاثة الذين أضفناهم في هذا القسم، بعد أن نراهم أولاً:

```sql
SELECT id, name FROM students WHERE id > 6;

BEGIN;
DELETE FROM students WHERE id > 6 RETURNING id, name;
COMMIT;

```

```text
 id |  name
----+--------
  9 | Reem
 10 | Yousef
 11 | Maha
(3 rows)

BEGIN
 id |  name
----+--------
  9 | Reem
 10 | Yousef
 11 | Maha
(3 rows)

DELETE 3
COMMIT

```

⚠️

قبل أي UPDATE أو DELETE على بيانات حقيقية: شغل نفس الشرط أولاً في SELECT count(\*) واكتب الرقم الذي تتوقعه، ثم نفذ الجملة داخل BEGIN، وقارن عدد الصفوف الذي تطبعه قاعدة البيانات بالرقم الذي كتبته، ولا تكتب COMMIT إلا إذا تطابقا.

## الصلاحيات GRANT وREVOKE

التطبيق لا يجب أن يتصل بقاعدة البيانات بالمستخدم postgres الذي يستطيع كل شيء، وإنما بمستخدم له الصلاحيات التي يحتاجها فقط، فإذا سرق مخترق باسورد أداة التقارير فلن يستطيع أن يحذف شيئاً. والمثال التالي ينشئ دوراً Role للتقارير، ويعطيه صلاحية قراءة جدول الدورات فقط، ثم يتصرف كأنه هو بالأمر `SET ROLE`:

```sql
CREATE ROLE report_reader LOGIN PASSWORD 'lab';
GRANT SELECT ON courses TO report_reader;
SET ROLE report_reader;
SELECT title, price FROM courses WHERE id = 1;
SELECT name FROM students WHERE id = 1;
RESET ROLE;
REVOKE SELECT ON courses FROM report_reader;
DROP ROLE report_reader;

```

```text
CREATE ROLE
GRANT
SET
    title     | price
--------------+--------
 Linux Basics | 300.00
(1 row)

ERROR:  permission denied for table students
RESET
REVOKE
DROP ROLE

```

الدور قرأ الدورات ورفض له جدول الطلاب، وهذا ما نريده. وفي PostgreSQL المستخدم والمجموعة كلاهما دور Role، أما في MySQL فالمستخدم يعرف باسمه والجهاز الذي يتصل منه مثل `'report'@'10.0.0.%'`، وقد شرحنا تصميم الصلاحيات لفريق كامل في PostgreSQL للفرق: تثبيت آمن وصلاحيات واضحة.

## الاستعلام SELECT: اختيار الصفوف والأعمدة

الجملة SELECT تقول لقاعدة البيانات ما تريد، ولا تقول لها كيف تجده، وهذا هو الفرق الأساسي بين SQL ولغات البرمجة العادية: أنت تصف النتيجة، والمخطط Planner داخل قاعدة البيانات يختار الطريقة. وأبسط شكل لها هو كل الأعمدة من جدول واحد:

```sql
SELECT * FROM courses;

```

```text
 id |    title     | track | price  | capacity | instructor_id
----+--------------+-------+--------+----------+---------------
  1 | Linux Basics | ops   | 300.00 |       20 |             4
  2 | PostgreSQL   | data  | 450.00 |       15 |             3
  3 | Docker       | ops   | 400.00 |        3 |             5
  4 | Python       | dev   | 350.00 |       25 |             6
  5 | Kubernetes   | ops   | 600.00 |       10 |          NULL
(5 rows)

```

والنجمة `*` مناسبة في psql وأنت تستكشف، ولكن لا تقم بكتابتها في كود التطبيق، والسبب أن الاستعلام يجلب أعمدة لا تحتاجها، ويتغير شكله بصمت إذا أضاف أحد عموداً للجدول. والأفضل أن تسمي الأعمدة، ويمكنك أن تحسب عموداً جديداً من عمود موجود، وأن تعطي أي عمود اسماً مستعاراً Alias بالكلمة AS:

```sql
SELECT title AS course, price AS "Price (SAR)", price * 1.15 AS with_vat FROM courses;

```

```text
    course    | Price (SAR) | with_vat
--------------+-------------+----------
 Linux Basics |      300.00 | 345.0000
 PostgreSQL   |      450.00 | 517.5000
 Docker       |      400.00 | 460.0000
 Python       |      350.00 | 402.5000
 Kubernetes   |      600.00 | 690.0000
(5 rows)

```

لاحظ أن الاسم الذي فيه مسافة وحروف كبيرة يكتب بين علامتي تنصيص مزدوجة `"Price (SAR)"`، لأن العلامة المزدوجة في PostgreSQL وفي المعيار هي لأسماء الأعمدة والجداول، أما المفردة فهي للنصوص، وخلطهما من أشهر أخطاء البداية كما سوف ترى في [قسم الأخطاء](#mistakes).

### اختيار الصفوف بالشرط WHERE

الشرط WHERE يختار الصفوف التي تريدها، ومعه دائماً ORDER BY حتى تكون النتيجة مرتبة بشكل تعرفه، والمثال التالي يجلب طلاب الرياض:

```sql
SELECT name, city FROM students WHERE city = 'Riyadh' ORDER BY name;

```

```text
 name |  city
------+--------
 Lina | Riyadh
 Sara | Riyadh
(2 rows)

```

وعمليات المقارنة هي `=` و`<>` للاختلاف، ويقبل PostgreSQL أيضاً `!=`، ثم `<` و`>` و`<=` و`>=`، والمثال التالي يجلب كل الدورات ما عدا التي سعرها 400:

```sql
SELECT title, price FROM courses WHERE price <> 400 ORDER BY price;

```

```text
    title     | price
--------------+--------
 Linux Basics | 300.00
 Python       | 350.00
 PostgreSQL   | 450.00
 Kubernetes   | 600.00
(4 rows)

```

والنطاق BETWEEN يشمل الطرفين، فالشرط التالي يعني السعر من 350 حتى 450 بما فيهما:

```sql
SELECT title, price FROM courses WHERE price BETWEEN 350 AND 450 ORDER BY price;

```

```text
   title    | price
------------+--------
 Python     | 350.00
 Docker     | 400.00
 PostgreSQL | 450.00
(3 rows)

```

والقائمة IN أقصر من كتابة OR عدة مرات:

```sql
SELECT name, city FROM students WHERE city IN ('Jeddah', 'Dammam') ORDER BY name;

```

```text
  name  |  city
--------+--------
 Huda   | Jeddah
 Khalid | Dammam
 Omar   | Jeddah
(3 rows)

```

والبحث داخل النص هو [LIKE](https://www.postgresql.org/docs/18/functions-matching.html?ref=arabroot.io)، وفيه رمزان: النسبة `%` تعني أي عدد من الحروف حتى صفر، والشرطة السفلية `_` تعني حرفاً واحداً بالضبط. فالشرط الأول يجلب الأسماء التي تنتهي بحرف a، والثاني الأسماء التي حرفها الثاني a:

```sql
SELECT name FROM students WHERE name LIKE '%a' ORDER BY name;
SELECT name FROM students WHERE name LIKE '_a%' ORDER BY name;

```

```text
 name
------
 Huda
 Lina
 Sara
(3 rows)

  name
--------
 Faisal
 Sara
(2 rows)

```

والآن لنبحث عن الأسماء التي تبدأ بحرف s صغير:

```sql
SELECT name FROM students WHERE name LIKE 's%';
SELECT name FROM students WHERE name ILIKE 's%';

```

```text
 name
------
(0 rows)

 name
------
 Sara
(1 row)

```

المقارنة في PostgreSQL تفرق بين الحرف الكبير والصغير، فلم تجد LIKE سارة، أما ILIKE فلا تفرق، وهي خاصة بـ PostgreSQL. وهنا يختلف المحركان الآخران كثيراً، فقد أعطى MySQL 8.4 النتيجة 1 للمقارنة `'Sara' = 'sara'` لأن الترتيب Collation الافتراضي فيه لا يفرق بين الحروف، وكذلك SQL Server 2022 بترتيبه الافتراضي `SQL_Latin1_General_CP1_CI_AS`، والحرفان `CI` في اسمه تعنيان Case Insensitive، أما SQLite فيفرق في `=` ولا يفرق في LIKE للحروف الإنجليزية فقط. لذلك نفس الاستعلام قد يجد صفاً في محرك ولا يجده في آخر.

وأخيراً جمع الشروط بـ AND وOR، وهنا فخ مشهور، فلنفرض أنك تريد دورات العمليات أو البيانات التي سعرها أكبر من 500، فتكتب:

```sql
SELECT title, track, price FROM courses
WHERE track = 'ops' OR track = 'data' AND price > 500
ORDER BY price;

```

```text
    title     | track | price
--------------+-------+--------
 Linux Basics | ops   | 300.00
 Docker       | ops   | 400.00
 Kubernetes   | ops   | 600.00
(3 rows)

```

ظهرت دورات بسعر 300 و400، والسبب أن AND تنفذ قبل OR، تماماً مثل الضرب قبل الجمع في الحساب، كما يبين [جدول أولوية العمليات](https://www.postgresql.org/docs/18/sql-syntax-lexical.html?ref=arabroot.io#SQL-PRECEDENCE)، فقرأت قاعدة البيانات الشرط هكذا: `track = 'ops' OR (track = 'data' AND price > 500)`. والحل أن تكتب الأقواس بنفسك:

```sql
SELECT title, track, price FROM courses
WHERE (track = 'ops' OR track = 'data') AND price > 500;

```

```text
   title    | track | price
------------+-------+--------
 Kubernetes | ops   | 600.00
(1 row)

```

والقاعدة: كلما خلطت AND مع OR في شرط واحد فاكتب الأقواس، حتى لو كنت متأكداً من الأولوية، لأن من يقرأ الكود بعدك قد لا يكون متأكداً.

### الترتيب والصفحات ORDER BY وLIMIT

الترتيب ORDER BY يقبل أكثر من عمود، فيرتب بالأول ثم يرتب الصفوف المتساوية فيه بالثاني، والكلمة DESC تعكس الترتيب، والافتراضي ASC أي تصاعدي:

```sql
SELECT title, track, price FROM courses ORDER BY track, price DESC;

```

```text
    title     | track | price
--------------+-------+--------
 PostgreSQL   | data  | 450.00
 Python       | dev   | 350.00
 Kubernetes   | ops   | 600.00
 Docker       | ops   | 400.00
 Linux Basics | ops   | 300.00
(5 rows)

```

وإذا أردت أول عدد من الصفوف فقط، مثل أغلى ثلاث دورات، فاستخدم [LIMIT](https://www.postgresql.org/docs/18/queries-limit.html?ref=arabroot.io)، وللصفحة الثانية وما بعدها أضف OFFSET وهو عدد الصفوف التي تتخطاها:

```sql
SELECT title, price FROM courses ORDER BY price DESC LIMIT 3;
SELECT id, title FROM courses ORDER BY id LIMIT 2 OFFSET 2;

```

```text
   title    | price
------------+--------
 Kubernetes | 600.00
 PostgreSQL | 450.00
 Docker     | 400.00
(3 rows)

 id | title
----+--------
  3 | Docker
  4 | Python
(2 rows)

```

في الأمثلة أعلاه لاحظ التالي:

- دون ORDER BY لا يوجد أي ضمان لترتيب الصفوف، حتى لو ظهرت مرتبة في تجربتك، لذلك LIMIT دون ORDER BY تعطيك «أي ثلاثة صفوف» وليس الثلاثة الأولى، وكذلك الصفحات دون ترتيب ثابت قد تكرر صفاً أو تتخطاه.
- الكلمة LIMIT موجودة في PostgreSQL وMySQL وSQLite، أما SQL Server فيستخدم [TOP](https://learn.microsoft.com/en-us/sql/t-sql/queries/top-transact-sql?ref=arabroot.io) مثل `SELECT TOP 3 …`، أو صيغة المعيار `OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY` بعد ORDER BY، وقد شغلناهما على SQL Server 2022\. وصيغة المعيار `FETCH FIRST 3 ROWS ONLY` يقبلها PostgreSQL أيضاً وأعطت نفس الدورات الثلاث.
- الصفحات بـ OFFSET تصبح بطيئة في الصفحات البعيدة، لأن قاعدة البيانات تقرأ كل الصفوف التي تتخطاها ثم ترميها، وهذا موضوع نرجع إليه في مقال الفهارس.

### حذف التكرار DISTINCT

عندما تطلب المدن من جدول الطلاب فسوف تتكرر كل مدينة بعدد طلابها، والكلمة DISTINCT تبقي كل قيمة مرة واحدة:

```sql
SELECT DISTINCT city FROM students ORDER BY city;

```

```text
  city
--------
 Abha
 Dammam
 Jeddah
 Riyadh
(4 rows)

```

وإذا كتبت بعدها أكثر من عمود فهي تحذف التكرار في الأعمدة معاً وليس في الأول فقط، فالمثال التالي يعطي كل مسار مع هل فيه دورة أغلى من 400 أم لا، ولاحظ أن ops ظهر مرتين لأن فيه الحالتين:

```sql
SELECT DISTINCT track, price > 400 AS expensive FROM courses ORDER BY track;

```

```text
 track | expensive
-------+-----------
 data  | t
 dev   | f
 ops   | t
 ops   | f
(4 rows)

```

ولا تستخدم DISTINCT لإخفاء صفوف مكررة لا تفهم سببها، فالتكرار غالباً علامة على ربط خاطئ بين الجداول، وهذا ما سوف نراه في الجزء الثاني.

## العبارات والدوال Functions

في SELECT يمكنك أن تحسب أي عبارة Expression على الأعمدة، ولكل محرك مئات الدوال الجاهزة، وسوف نمر على أكثرها استخداماً. ونبدأ بدوال النصوص من [توثيق دوال النصوص](https://www.postgresql.org/docs/18/functions-string.html?ref=arabroot.io):

```sql
SELECT name,
       upper(name)                  AS upper_name,
       length(name)                 AS len,
       name || ' (' || city || ')'  AS label,
       split_part(email, '@', 2)    AS domain
FROM students
ORDER BY id
LIMIT 3;

```

```text
 name | upper_name | len |     label     |   domain
------+------------+-----+---------------+-------------
 Sara | SARA       |   4 | Sara (Riyadh) | example.com
 Omar | OMAR       |   4 | Omar (Jeddah) | example.com
 Lina | LINA       |   4 | Lina (Riyadh) | example.com
(3 rows)

```

والعلامة `||` هي طريقة المعيار لدمج النصوص، ولكنها من أكثر المواضع اختلافاً بين المحركات، فقد أعطت الجملة `SELECT 'a' || 'b'` في MySQL 8.4 الرقم 0 مع تحذير، لأن `||` فيه تعني OR المنطقية، أما SQL Server فيدمج النصوص بعلامة `+`. والدالة `concat` موجودة في الأربعة، ولكن انتبه لسلوكها مع NULL:

```sql
SELECT concat('Sara', NULL, '!') AS with_concat, 'Sara' || NULL || '!' AS with_pipes;

```

```text
 with_concat | with_pipes
-------------+------------
 Sara!       | NULL
(1 row)

```

في PostgreSQL تتجاهل `concat` القيمة NULL، أما `||` فإذا دخلت فيها NULL أصبحت النتيجة كلها NULL، وأعطت `concat` نفس النتيجة `Sara!` في SQL Server وSQLite، أما في MySQL فأعطت NULL كما يذكر [توثيق دوال النصوص في MySQL](https://dev.mysql.com/doc/refman/8.4/en/string-functions.html?ref=arabroot.io). وهذا فرق يظهر عندما تبني اسماً كاملاً من ثلاثة أعمدة أحدها فارغ. وأما الأرقام فأشهر دوالها التقريب:

```sql
SELECT title,
       price * 1.15             AS raw_vat,
       round(price * 1.15, 1)   AS rounded,
       ceil(price * 1.15)       AS ceil_vat,
       floor(price * 1.15)      AS floor_vat
FROM courses
ORDER BY id;

```

```text
    title     | raw_vat  | rounded | ceil_vat | floor_vat
--------------+----------+---------+----------+-----------
 Linux Basics | 345.0000 |   345.0 |      345 |       345
 PostgreSQL   | 517.5000 |   517.5 |      518 |       517
 Docker       | 460.0000 |   460.0 |      460 |       460
 Python       | 402.5000 |   402.5 |      403 |       402
 Kubernetes   | 690.0000 |   690.0 |      690 |       690
(5 rows)

```

الدالة `round` تقرب لعدد الخانات الذي تعطيه، و`ceil` للأعلى دائماً، و`floor` للأسفل دائماً. ثم التواريخ من [توثيق دوال التاريخ](https://www.postgresql.org/docs/18/functions-datetime.html?ref=arabroot.io)، وفي PostgreSQL يمكنك أن تجمع عدد أيام على تاريخ، وأن تطرح تاريخين فتحصل على عدد الأيام بينهما:

```sql
SELECT enrolled_on,
       enrolled_on + 30                       AS plus_30_days,
       DATE '2026-10-01' - enrolled_on        AS days_until_oct,
       extract(dow FROM enrolled_on)          AS day_of_week,
       to_char(enrolled_on, 'YYYY-MM')        AS month,
       date_trunc('month', enrolled_on)::date AS month_start
FROM enrollments
WHERE id IN (1, 4, 9)
ORDER BY id;

```

```text
 enrolled_on | plus_30_days | days_until_oct | day_of_week |  month  | month_start
-------------+--------------+----------------+-------------+---------+-------------
 2026-09-01  | 2026-10-01   |             30 |           2 | 2026-09 | 2026-09-01
 2026-09-10  | 2026-10-10   |             21 |           4 | 2026-09 | 2026-09-01
 2026-09-12  | 2026-10-12   |             19 |           6 | 2026-09 | 2026-09-01
(3 rows)

```

في المخرج أعلاه لاحظ التالي:

- الدالة `extract(dow …)` ترجع رقم اليوم في الأسبوع بدءاً من الأحد بالرقم 0، فالأول من سبتمبر 2026 يوم ثلاثاء.
- الدالة `date_trunc` تقص التاريخ إلى بداية الشهر، وهي التي سوف تستخدمها في التقارير الشهرية في الجزء الثاني.
- الصيغة `DATE '2026-10-01'` هي طريقة المعيار لكتابة تاريخ ثابت، وأما دوال التاريخ نفسها فتختلف أسماؤها كثيراً بين المحركات، فمثلاً يقابل طرح التاريخين في SQL Server الدالة `DATEDIFF`، وأما المناطق الزمنية والتقويم الهجري فلهما مقال مستقل هو [التاريخ والوقت للمطورين](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%A7%D8%B1%D9%8A%D8%AE-%D9%88%D8%A7%D9%84%D9%88%D9%82%D8%AA-utc-%D9%88%D8%A7%D9%84%D9%85%D9%86%D8%A7%D8%B7%D9%82-%D8%A7%D9%84%D8%B2%D9%85%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D8%A7%D9%84%D9%87%D8%AC%D8%B1%D9%8A/).

### القرار داخل الاستعلام CASE وCOALESCE

العبارة [CASE](https://www.postgresql.org/docs/18/functions-conditional.html?ref=arabroot.io) هي if داخل SQL، وتمر على الشروط بالترتيب وتأخذ أول شرط يتحقق، والمثال التالي يعطي كل درجة وصفاً:

```sql
SELECT student_id, course_id, grade,
       CASE
           WHEN grade >= 90   THEN 'excellent'
           WHEN grade >= 75   THEN 'good'
           WHEN grade IS NULL THEN 'in progress'
           ELSE 'pass'
       END AS level
FROM enrollments
ORDER BY id;

```

```text
 student_id | course_id | grade |    level
------------+-----------+-------+-------------
          1 |         1 |    90 | excellent
          1 |         2 |    85 | good
          2 |         1 |    70 | pass
          2 |         3 |  NULL | in progress
          3 |         2 |    95 | excellent
          3 |         4 |  NULL | in progress
          4 |         4 |    60 | pass
          5 |         2 |  NULL | in progress
          5 |         3 |    88 | good
(9 rows)

```

لاحظ أن ترتيب الشروط مهم، فالدرجة 95 تحقق الشرطين الأول والثاني، وأخذت الأول. والدالة COALESCE ترجع أول قيمة ليست NULL من القيم التي تعطيها، وهي الطريقة المعتادة لعرض قيمة بديلة:

```sql
SELECT name, COALESCE(phone, 'no phone') AS phone FROM students ORDER BY id;

```

```text
  name  |   phone
--------+------------
 Sara   | 0500000001
 Omar   | no phone
 Lina   | 0500000003
 Khalid | no phone
 Huda   | 0500000005
 Faisal | 0500000006
(6 rows)

```

وعكسها الدالة NULLIF، فهي ترجع NULL إذا تساوت القيمتان، وأشهر استخدام لها هو منع القسمة على صفر:

```sql
SELECT 10 / 0;
SELECT 10 / NULLIF(0, 0) AS safe_div;

```

```text
ERROR:  division by zero
 safe_div
----------
     NULL
(1 row)

```

والقسمة على صفر خطأ في PostgreSQL وSQL Server الذي أعطى `Divide by zero error encountered`، أما MySQL وSQLite فأعطيا NULL مع تحذير في MySQL، أي أن نفس التقرير قد يتوقف في محرك ويعطي خانة فارغة في آخر.

## القيمة NULL والمنطق الثلاثي

القيمة NULL لا تعني صفراً ولا نصاً فارغاً، وإنما تعني «غير معروف»: عمر لديه رقم جوال على الأغلب، ولكننا لا نعرفه. وهذا المعنى هو سبب أغرب سلوك في SQL، فلنفرض أنك تريد الطلاب الذين لم يسجلوا رقم جوال، فتكتب الشرط كما تكتبه في أي لغة برمجة:

```sql
SELECT name FROM students WHERE phone = NULL;

```

```text
 name
------
(0 rows)

```

لا يوجد خطأ، ولا يوجد أي طالب، مع أن عمر وخالد بلا رقم. والسبب أن مقارنة أي شيء بـ «غير معروف» نتيجتها «غير معروف»، حتى NULL = NULL، والشرط WHERE لا يمرر الصف إلا إذا كانت النتيجة TRUE. والطريقة الصحيحة هي [IS NULL](https://www.postgresql.org/docs/18/functions-comparison.html?ref=arabroot.io):

```sql
SELECT name FROM students WHERE phone IS NULL;

```

```text
  name
--------
 Omar
 Khalid
(2 rows)

```

ولكي ترى المنطق الثلاثي Three-Valued Logic بنفسك اطلب من قاعدة البيانات أن تحسب بعض العبارات مباشرة:

```sql
SELECT NULL = NULL    AS eq,
       NULL <> 1      AS ne,
       NULL AND false AS and_false,
       NULL OR true   AS or_true,
       NULL IS NULL   AS is_null;

```

```text
  eq  |  ne  | and_false | or_true | is_null
------+------+-----------+---------+---------
 NULL | NULL | f         | t       | t
(1 row)

```

لاحظ أن `NULL AND false` نتيجتها FALSE، لأنه مهما كانت القيمة المجهولة فالنتيجة خاطئة، وكذلك `NULL OR true` نتيجتها TRUE، وهذا هو المنطق الثلاثي الذي يشرحه [توثيق العمليات المنطقية](https://www.postgresql.org/docs/18/functions-logical.html?ref=arabroot.io). والفخ الأخطر أن الشرط العكسي يخفي الصفوف أيضاً، فإذا طلبت كل الطلاب الذين رقمهم ليس رقم سارة:

```sql
SELECT name FROM students WHERE phone <> '0500000001';

```

```text
  name
--------
 Lina
 Huda
 Faisal
(3 rows)

```

اختفى عمر وخالد، مع أن رقمهما بكل تأكيد ليس رقم سارة. وإذا كنت تريدهما في النتيجة فاستخدم `IS DISTINCT FROM` التي تعامل NULL كقيمة عادية في المقارنة:

```sql
SELECT name FROM students WHERE phone IS DISTINCT FROM '0500000001';

```

```text
  name
--------
 Omar
 Lina
 Khalid
 Huda
 Faisal
(5 rows)

```

والصورة التالية تجمع جداول المنطق الثلاثي، وما يحدث لكل طالب في الشرط السابق:

![أربعة جداول صغيرة: المقارنات حيث NULL = NULL وNULL <> 1 نتيجتهما NULL و1 = 1 وNULL IS NULL نتيجتهما TRUE، وجدول AND حيث TRUE AND NULL تساوي NULL وFALSE AND NULL تساوي FALSE، وجدول OR حيث TRUE OR NULL تساوي TRUE وFALSE OR NULL تساوي NULL، وجدول NOT حيث NOT NULL تساوي NULL. وتحتها الطلاب الستة مع الشرط phone <> '0500000001': سارة FALSE، وعمر وخالد NULL فيحذفون، ولينا وهدى وفيصل TRUE فيمرون إلى النتيجة](https://arabroot.io/content/images/2026/10/sql-basics-03-null-logic.webp)

الشرط WHERE يرمي الصف الذي نتيجته NULL كما يرمي الذي نتيجته FALSE

وآخر مكان تظهر فيه NULL في هذا الجزء هو الترتيب، فـ PostgreSQL يضعها في آخر الترتيب التصاعدي:

```sql
SELECT student_id, course_id, grade FROM enrollments ORDER BY grade;

```

```text
 student_id | course_id | grade
------------+-----------+-------
          4 |         4 |    60
          2 |         1 |    70
          1 |         2 |    85
          5 |         3 |    88
          1 |         1 |    90
          3 |         2 |    95
          3 |         4 |  NULL
          5 |         2 |  NULL
          2 |         3 |  NULL
(9 rows)

```

ويقول [توثيق الترتيب](https://www.postgresql.org/docs/18/queries-order.html?ref=arabroot.io) إن NULL تعامل كأنها أكبر من أي قيمة، لذلك تأتي في آخر ASC وأول DESC، ويمكنك تغيير ذلك بـ `NULLS FIRST` أو `NULLS LAST`. أما MySQL وSQL Server وSQLite فيفعلون العكس، فقد شغلنا جدولاً صغيراً فيه 60 و90 وNULL على الثلاثة فظهرت NULL أولاً في الترتيب التصاعدي في كل منها، وهذا ما يذكره [توثيق ORDER BY في SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql?ref=arabroot.io): تعامل NULL كأصغر قيمة ممكنة. ويقبل SQLite الكلمتين `NULLS FIRST` و`NULLS LAST` كما يذكر [توثيق SELECT فيه](https://www.sqlite.org/lang%5Fselect.html?ref=arabroot.io)، أما MySQL وSQL Server فلا يقبلانهما، لذلك إذا كان تقريرك ينتقل بين قاعدتي بيانات فاكتب موضع NULL صراحة حيث يدعمه المحرك. وأما NULL في الدوال التجميعية مثل `count` و`avg` فهي موضوع [الجزء الثاني](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-2-%D8%A7%D9%84%D8%B1%D8%A8%D8%B7-join-%D9%88%D8%A7%D9%84%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D9%84%D8%A7%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D8%B9%D9%8A%D8%A9/#aggregate-null).

## بأي ترتيب تنفذ قاعدة البيانات الجملة؟

نكتب الجملة بالترتيب SELECT ثم FROM ثم WHERE، ولكن قاعدة البيانات تقرؤها بترتيب آخر، وفهم هذا الترتيب يفسر نصف رسائل الخطأ التي ستراها. والمثال التالي يحسب السعر مع ضريبة 15% ويعطيه اسماً مستعاراً هو `with_vat`، ثم يحاول أن يستخدم الاسم في الشرط:

```sql
SELECT title, price * 1.15 AS with_vat
FROM courses
WHERE with_vat > 400;

```

```text
ERROR:  column "with_vat" does not exist
LINE 3: WHERE with_vat > 400;
              ^

```

وقد يتساءل البعض: كيف لا يوجد العمود وقد عرفناه في نفس الجملة؟ والإجابة أن [توثيق SELECT](https://www.postgresql.org/docs/18/sql-select.html?ref=arabroot.io) يرتب المعالجة بهذا الشكل: يحسب أولاً FROM والربط، ثم يحذف الصفوف بالشرط WHERE، ثم يجمع GROUP BY ويطبق HAVING، وبعدها فقط يحسب أعمدة SELECT وأسماءها المستعارة، ثم يرتب ORDER BY ثم يقطع LIMIT. ويقول التوثيق صراحة إن اسم العمود في المخرج يمكن استخدامه في ORDER BY وGROUP BY ولكن ليس في WHERE أو HAVING، لذلك نكرر العبارة في الشرط ونستخدم الاسم في الترتيب:

```sql
SELECT title, price * 1.15 AS with_vat
FROM courses
WHERE price * 1.15 > 400
ORDER BY with_vat;

```

```text
   title    | with_vat
------------+----------
 Python     | 402.5000
 Docker     | 460.0000
 PostgreSQL | 517.5000
 Kubernetes | 690.0000
(4 rows)

```

والصورة التالية تبين الترتيب الذي نكتب به والترتيب الذي تطبقه قاعدة البيانات:

![صف علوي بعنوان ما تكتبه فيه SELECT ثم FROM وJOIN ثم WHERE ثم GROUP BY ثم HAVING ثم ORDER BY ثم LIMIT، وصف سفلي مرقم بالترتيب الذي يطبقه PostgreSQL: 1 FROM وJOIN لبناء الصفوف، 2 WHERE لحذف صفوف، 3 GROUP BY لتكوين المجموعات، 4 HAVING لحذف مجموعات، 5 SELECT لحساب الأعمدة والأسماء المستعارة، 6 ORDER BY للترتيب، 7 LIMIT للقطع. وتحتهما مربع أحمر فيه خطأ with_vat في WHERE لأن الخطوة 2 تسبق الخطوة 5، ومربع أخضر فيه الحل باستخدام الاسم في ORDER BY](https://arabroot.io/content/images/2026/10/sql-basics-02-query-order.webp)

WHERE تعمل قبل SELECT، لذلك لا ترى الأسماء المستعارة، وORDER BY تعمل بعدها فتراها

يجدر الإشارة هنا أن هذا ترتيب منطقي يحدد ما يراه كل جزء من الجملة، وليس ترتيب التنفيذ الفعلي، فالمخطط حر أن يستخدم فهرساً ويرتب ويقطع في خطوة واحدة طالما أن النتيجة نفسها، وهذا ما تراه في خطة التنفيذ EXPLAIN التي شرحناها في مقال [PostgreSQL للجداول الضخمة](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/#explain). والخطوات GROUP BY وHAVING سوف نشرحها في الجزء الثاني، وعندها سوف يتضح لك لماذا ترتيبها بين WHERE وSELECT.

## أخطاء شائعة في البداية

هذه أخطاء يقع فيها كل من يبدأ بلغة SQL، وبعضها يعطي رسالة خطأ واضحة، وبعضها أخطر لأنه يعطي نتيجة خاطئة بصمت. وأولها فاصلة زائدة قبل FROM، وهي تحدث عندما تحذف آخر عمود من القائمة:

```sql
SELECT name, city, FROM students;

```

```text
ERROR:  syntax error at or near "FROM"
LINE 1: SELECT name, city, FROM students;
                           ^

```

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

```sql
SELECT name FROM students WHERE city = "Riyadh";

```

```text
ERROR:  column "Riyadh" does not exist
LINE 1: SELECT name FROM students WHERE city = "Riyadh";
                                               ^

```

فهم PostgreSQL أن `"Riyadh"` اسم عمود، أما MySQL فقد قبل نفس الجملة وأعاد النص لأن العلامة المزدوجة فيه للنصوص افتراضياً، وهذا يجعل من يتعلم على MySQL يتفاجأ عندما ينتقل. والخطأ الثالث علامة التنصيص داخل النص، مثل اسم O'Brien، فالعلامة الثانية تغلق النص قبل نهايته، وقد أرسلنا الجملة من الطرفية حتى ترى رد السيرفر:

```bash
docker exec lab-sql-pg psql -U postgres -d academy -c "SELECT 'O'Brien' AS name;"

```

```text
ERROR:  unterminated quoted string at or near "' AS name;"
LINE 1: SELECT 'O'Brien' AS name;
                       ^

```

والحل داخل SQL أن تكتب العلامة مرتين:

```sql
SELECT 'O''Brien' AS name;

```

```text
  name
---------
 O'Brien
(1 row)

```

ولكن لا تقم بذلك في كود التطبيق، وإنما أرسل القيمة كمعامل Parameter منفصل عن الجملة، والسبب أن نفس هذه العلامة هي باب حقن SQL الذي نشرحه في [الجزء الثالث](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/#sql-injection). والخطأ الرابع أخطر لأنه بلا رسالة، وهو القسمة الصحيحة:

```sql
SELECT title, capacity / 2 AS half, capacity / 2.0 AS half_exact FROM courses WHERE id = 3;

```

```text
 title  | half |     half_exact
--------+------+--------------------
 Docker |    1 | 1.5000000000000000
(1 row)

```

وبقية الأخطاء الصامتة رأيتها في الأقسام السابقة، وهذه قائمتها حتى ترجع إليها:

- المقارنة `= NULL` بدل `IS NULL` تعطي صفر صفوف دون خطأ.
- الشرط `<>` يخفي الصفوف التي فيها NULL، فاستخدم `IS DISTINCT FROM` إذا كنت تريدها.
- خلط AND مع OR دون أقواس يغير معنى الشرط.
- LIMIT دون ORDER BY يعطي صفوفاً عشوائية في الواقع.
- المقارنة `name = 'sara'` لا تجد Sara في PostgreSQL، وتجدها في MySQL وSQL Server.
- UPDATE أو DELETE دون WHERE ينفذ على الجدول كله، فاعمل داخل BEGIN واقرأ عدد الصفوف.
- عدم وجود قيد في الجدول يعني أن البيانات الخاطئة سوف تدخل يوماً ما، مهما كان الكود حريصاً.

## تمارين

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

**التمرين 1:** اعرض عنوان وسعر دورات مسار العمليات ops من الأرخص للأغلى.

الحل Answer 

```sql
SELECT title, price
FROM courses
WHERE track = 'ops'
ORDER BY price;

```

```text
    title     | price
--------------+--------
 Linux Basics | 300.00
 Docker       | 400.00
 Kubernetes   | 600.00
(3 rows)

```

**التمرين 2:** اعرض الطلاب من الرياض أو جدة الذين لديهم رقم جوال، مرتبين بالاسم.

الحل Answer 

```sql
SELECT name, city, phone
FROM students
WHERE (city = 'Riyadh' OR city = 'Jeddah')
  AND phone IS NOT NULL
ORDER BY name;

```

```text
 name |  city  |   phone
------+--------+------------
 Huda | Jeddah | 0500000005
 Lina | Riyadh | 0500000003
 Sara | Riyadh | 0500000001
(3 rows)

```

بدون الأقواس سوف يظهر عمر لأن شرط الجوال يرتبط بجدة فقط.

**التمرين 3:** اعرض عنوان كل دورة وسعرها مع الضريبة 15% مقرباً لأقرب ريال، للدورات التي سعرها مع الضريبة أقل من 500 فقط، من الأرخص للأغلى.

الحل Answer 

```sql
SELECT title, round(price * 1.15) AS with_vat
FROM courses
WHERE price * 1.15 < 500
ORDER BY with_vat;

```

```text
    title     | with_vat
--------------+----------
 Linux Basics |      345
 Python       |      403
 Docker       |      460
(3 rows)

```

كررنا العبارة في WHERE لأن الاسم المستعار لا يوجد بعد في تلك الخطوة، واستخدمناه في ORDER BY.

**التمرين 4:** اعرض رقم الطالب ورقم الدورة للتسجيلات التي لم تحصل على درجة بعد.

الحل Answer 

```sql
SELECT student_id, course_id
FROM enrollments
WHERE grade IS NULL
ORDER BY student_id;

```

```text
 student_id | course_id
------------+-----------
          2 |         3
          3 |         4
          5 |         2
(3 rows)

```

**التمرين 5:** صفحة الطلاب في التطبيق تعرض طالبين في كل صفحة مرتبين بالاسم، اكتب استعلام الصفحة الثانية.

الحل Answer 

```sql
SELECT name
FROM students
ORDER BY name
LIMIT 2 OFFSET 2;

```

```text
  name
--------
 Khalid
 Lina
(2 rows)

```

**التمرين 6:** اعرض كل دورة مع تصنيف premium إذا كان سعرها 450 أو أكثر وstandard لغيرها، من الأغلى للأرخص.

الحل Answer 

```sql
SELECT title,
       CASE WHEN price >= 450 THEN 'premium' ELSE 'standard' END AS tier
FROM courses
ORDER BY price DESC;

```

```text
    title     |   tier
--------------+----------
 Kubernetes   | premium
 PostgreSQL   | premium
 Docker       | standard
 Python       | standard
 Linux Basics | standard
(5 rows)

```

**التمرين 7:** المعهد يريد رفع سعة دورة Docker إلى 5 مقاعد، اكتب الخطوات الآمنة لذلك، ثم ألغ التغيير حتى تبقى البيانات كما هي.

الحل Answer 

```sql
BEGIN;
SELECT count(*) FROM courses WHERE title = 'Docker';
UPDATE courses SET capacity = 5
WHERE title = 'Docker'
RETURNING title, capacity;
ROLLBACK;

```

```text
BEGIN
 count
-------
     1
(1 row)

 title  | capacity
--------+----------
 Docker |        5
(1 row)

UPDATE 1
ROLLBACK

```

توقعنا صفاً واحداً فوجدنا `UPDATE 1`، ولو كان التغيير حقيقياً لكتبنا COMMIT.

**التمرين 8:** اكتب جدولاً للقاعات `rooms` فيه رقم يولد تلقائياً، واسم إلزامي لا يتكرر، وعدد مقاعد إلزامي أكبر من صفر، ثم أضف قاعتين في جملة واحدة، ثم تأكد أن القيد يرفض قاعة بلا مقاعد، ثم احذف الجدول.

الحل Answer 

```sql
CREATE TABLE rooms (
    id     int   GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name   text  NOT NULL UNIQUE,
    seats  int   NOT NULL CHECK (seats > 0)
);
INSERT INTO rooms (name, seats) VALUES ('Lab A', 20), ('Lab B', 12);
INSERT INTO rooms (name, seats) VALUES ('Lab C', 0);
DROP TABLE rooms;

```

```text
CREATE TABLE
INSERT 0 2
ERROR:  new row for relation "rooms" violates check constraint "rooms_seats_check"
DETAIL:  Failing row contains (3, Lab C, 0).
DROP TABLE

```

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

```bash
docker rm -f lab-sql-pg

```

## الخلاصة

وصلنا لنهاية الجزء الأول، وأهم ما تخرج به منه:

- قاعدة البيانات العلائقية تحفظ البيانات في جداول، والمفتاح الأساسي يعرف كل صف، والمفتاح الأجنبي يربط الصفوف بالقيم نفسها، وهذه فكرة Codd عام 1970 التي بنيت عليها لغة SEQUEL ثم SQL.
- SQL معيار دولي آخر إصداراته SQL:2023، ولكل محرك لهجته، والأساسيات تعمل في كل المحركات تقريباً كما هي.
- الأوامر خمس مجموعات: DDL للهيكل، وDML للصفوف، وDQL للقراءة، وDCL للصلاحيات، وTCL للمعاملات.
- اختر النوع المناسب لكل عمود، والمال numeric دائماً، واكتب القيود في الجدول نفسه لأنها تعمل على كل تطبيق وكل شخص.
- نفذ UPDATE وDELETE داخل BEGIN وقارن عدد الصفوف بما تتوقعه قبل COMMIT.
- القيمة NULL تعني «غير معروف»، فاستخدم IS NULL وIS DISTINCT FROM وCOALESCE، وتذكر أن موضعها في الترتيب يختلف بين المحركات.
- قاعدة البيانات تقرأ الجملة بالترتيب FROM ثم WHERE ثم GROUP BY ثم HAVING ثم SELECT ثم ORDER BY ثم LIMIT، وهذا يفسر لماذا لا ترى WHERE الأسماء المستعارة.

وفي الجزء التالي سوف نجمع الصفوف من أكثر من جدول بالربط JOIN، ونحسب المجاميع والمتوسطات بالتجميع GROUP BY، ونرى كيف يتضاعف رقم في تقرير دون أي رسالة خطأ. وإذا أردت أن تطبق ما قرأته على قاعدة بيانات أكبر، ف[تمارين Kata في SQL](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-sql-%D9%88%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/) مكتوبة لهذا الغرض، و[المستوى الأول منها](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-sql-%D9%88%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/#level-1) يناسب هذا الجزء:

[![](https://arabroot.io/content/images/2026/10/sql-joins-cover-cover.webp)دليلتعلم SQL (2): الربط JOIN والتجميع والاستعلامات الفرعيةكيف تجمع الصفوف من أكثر من جدول وتلخصها بالتجميع والاستعلامات الفرعية، دون أن تتضاعف أرقام تقاريرك.](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-2-%D8%A7%D9%84%D8%B1%D8%A8%D8%B7-join-%D9%88%D8%A7%D9%84%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D9%84%D8%A7%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D8%B9%D9%8A%D8%A9/)[![](https://arabroot.io/content/images/2026/10/sql-advanced-cover-cover.webp)دليلتعلم SQL (3): CTE ودوال النوافذ والمعاملاتكيف تكتب استعلامات أوضح بجمل WITH ودوال النوافذ، وتحمي بياناتك بالمعاملات ومستويات العزل ومن حقن SQL.](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/katas-sql-cover-cover.webp)دليلتمارين Kata في SQL بثلاثة مستويات مع الحلولتمارين SQL تحلها بيدك دون ذكاء اصطناعي على متجر من مئتي ألف طلب، من فخاخ LEFT JOIN إلى اختيار الفهرس المناسب.](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D9%81%D9%8A-sql-%D9%88%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/db-design-cover-cover.webp)دليلتصميم قواعد البيانات: التطبيع Normalization ومتى تكسرهلماذا يفشل الجدول الواحد الكبير، وكيف تقسمه إلى جداول لا تناقض نفسها، وما الذي يتغير عندما تكبر البيانات.](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/)

وبعد الأجزاء الثلاثة تكمل مرحلة [لغات البرمجة](https://arabroot.io/start/#stage-programming) بمقال [الخيوط Threads والتزامن](https://arabroot.io/articles/%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-threads-%D9%88%D8%A7%D9%84%D8%A3%D9%82%D9%81%D8%A7%D9%84-%D9%88-async-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/)، وفيه نفس مشكلة الجلستين اللتين تعملان على نفس البيانات في الجزء الثالث، ولكن داخل ذاكرة برنامجك.

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ كل الاستعلامات والتجارب على جلستين في PostgreSQL 18.6 (postgres:18، Debian 18.6-1.pgdg13+2)، والمقارنات على MySQL 8.4.11 وSQL Server 2022 (16.0.4295.3، CU27) Developer Edition، في Docker Engine 29.8.0 (Docker Desktop).
- أكتوبر 2026: إعادة كتابة المقال كجزء أول من سلسلة «تعلم SQL» من ثلاثة أجزاء، بإضافة النموذج العلائقي وتاريخ اللغة ومجموعات الأوامر وأنواع البيانات والقيود والدوال وتمارين بحلولها، على قاعدة بيانات جديدة من خمسة جداول. ونقلنا الربط والتجميع والاستعلامات الفرعية إلى [الجزء الثاني](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-2-%D8%A7%D9%84%D8%B1%D8%A8%D8%B7-join-%D9%88%D8%A7%D9%84%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B9%D9%84%D8%A7%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D8%B9%D9%8A%D8%A9/)، ودوال النوافذ والمعاملات ومستويات العزل وحقن SQL إلى [الجزء الثالث](https://arabroot.io/articles/%D8%AA%D8%B9%D9%84%D9%85-sql-3-cte-%D9%88%D8%AF%D9%88%D8%A7%D9%84-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D9%81%D8%B0-%D9%88%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A7%D8%AA/). وأعدنا تشغيل كل الأمثلة على PostgreSQL 18.6 (postgres:18.6)، والمقارنات على MySQL 8.4.11 وSQL Server 2022 (16.0.4295.3، CU27) وSQLite 3.49.1.