طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 122 · برمجة قواعد البيانات
الدرس 8 · الوحدة 4: تصميم جداول البيانات بالعلاقات والفهارس

التطبيع: من جدول واحد إلى جداول مترابطة

لماذا يفشل الجدول الواحد الكبير؟ وكيف نفكّكه بقواعد واضحة دون أن نخسر البيانات؟ ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تكتشف التكرار وشذوذ الإدخال والتعديل والحذف في جدول غير مطبّع.
  • تطبّق قواعد 1NF و 2NF و 3NF.
  • تفكّك جدولًا موجودًا وتنقل بياناته بـ INSERT … SELECT DISTINCT.
  • تعرف متى يُقبل إلغاء التطبيع.

١جدول واحد لكل شيء؟

أسرع تصميم يخطر بالبال: جدول واحد فيه كل المعلومات. يبدو بسيطًا، لكنه يخفي مشكلات:

query.sql
CREATE TABLE sales_flat (  order_id      int,  order_date    date,  customer_name text,  customer_city text,  products      text,          -- قائمة مفصولة بفواصل!  supplier      text,  supplier_phone text);INSERT INTO sales_flat VALUES (1, '2025-09-01', 'سارة', 'الرياض', 'حاسب محمول, فأرة', 'مؤسسة النور', '0501111111'), (2, '2025-09-02', 'خالد', 'جدة',    'فأرة',             'مؤسسة النور', '0501111111'), (3, '2025-09-03', 'سارة', 'الرياض', 'دفتر',             'مكتبة الفكر', '0502222222');SELECT * FROM sales_flat;
النتيجة · 3 صفوف
order_idorder_datecustomer_namecustomer_cityproductssuppliersupplier_phone
12025-09-01سارةالرياضحاسب محمول, فأرةمؤسسة النور0501111111
22025-09-02خالدجدةفأرةمؤسسة النور0501111111
32025-09-03سارةالرياضدفترمكتبة الفكر0502222222
المشكلةمثال من الجدول
التكرارمدينة سارة ورقم «مؤسسة النور» مكتوبان أكثر من مرة
شذوذ التعديلتغيّر رقم المورد ← يجب تعديله في كل صف، وإن نسيت صفًّا صار للمورد رقمان
شذوذ الإدخاللا يمكن إضافة مورد جديد قبل أن يُطلب أحد منتجاته
شذوذ الحذفحذف الطلب 3 يحذف معه الشيء الوحيد الذي نعرفه عن «مكتبة الفكر»
قيم مركّبةعمود products فيه «حاسب محمول, فأرة»: لا نستطيع أن نسأل بسهولة «كم مرة بيعت الفأرة؟»

٢الصيغ الطبيعية

التطبيع Normalization تفكيك الجدول إلى جداول أصغر بحيث تُحفظ كل معلومة في مكان واحد. ونقيسه بثلاث صيغ متدرجة:

الصيغةالقاعدةفي مثالنا
1NF الأولىكل خلية قيمة واحدة ذرّية؛ لا قوائم ولا أعمدة مكررةالمنتجات تصبح صفوفًا في جدول البنود، لا نصًّا بفواصل
2NF الثانية1NF + كل عمود يعتمد على كامل المفتاح المركّب لا جزء منهاسم المنتج يعتمد على product_id وحده، فينتقل إلى products
3NF الثالثة2NF + لا يعتمد عمود عادي على عمود عادي آخررقم المورد يعتمد على المورد لا على الطلب، فينتقل إلى suppliers

٣التفكيك: من الجدول المسطّح إلى جداول مطبّعة

نقل البيانات الموجودة إلى الجداول الجديدة لا يحتاج كتابتها يدويًّا: INSERT … SELECT DISTINCT يأخذ القيم الفريدة من الجدول القديم.

query.sql
CREATE TABLE sales_flat (  order_id      int,  order_date    date,  customer_name text,  customer_city text,  products      text,          -- قائمة مفصولة بفواصل!  supplier      text,  supplier_phone text);INSERT INTO sales_flat VALUES (1, '2025-09-01', 'سارة', 'الرياض', 'حاسب محمول, فأرة', 'مؤسسة النور', '0501111111'), (2, '2025-09-02', 'خالد', 'جدة',    'فأرة',             'مؤسسة النور', '0501111111'), (3, '2025-09-03', 'سارة', 'الرياض', 'دفتر',             'مكتبة الفكر', '0502222222'); CREATE TABLE suppliers (  id    serial PRIMARY KEY,  name  text NOT NULL UNIQUE,  phone text);INSERT INTO suppliers (name, phone)SELECT DISTINCT supplier, supplier_phone FROM sales_flat; CREATE TABLE buyers (  id   serial PRIMARY KEY,  name text NOT NULL,  city text NOT NULL);INSERT INTO buyers (name, city)SELECT DISTINCT customer_name, customer_city FROM sales_flat; SELECT * FROM suppliers ORDER BY id;
النتيجة · صفّان
idnamephone
1مؤسسة النور0501111111
2مكتبة الفكر0502222222

الآن رقم المورد محفوظ مرة واحدة. تعديله سطر واحد، وإضافة مورد جديد لا تحتاج طلبًا.

والقائمة «حاسب محمول, فأرة» تُفكّ إلى صفوف بالدالة string_to_table:

query.sql
CREATE TABLE sales_flat (  order_id      int,  order_date    date,  customer_name text,  customer_city text,  products      text,          -- قائمة مفصولة بفواصل!  supplier      text,  supplier_phone text);INSERT INTO sales_flat VALUES (1, '2025-09-01', 'سارة', 'الرياض', 'حاسب محمول, فأرة', 'مؤسسة النور', '0501111111'), (2, '2025-09-02', 'خالد', 'جدة',    'فأرة',             'مؤسسة النور', '0501111111'), (3, '2025-09-03', 'سارة', 'الرياض', 'دفتر',             'مكتبة الفكر', '0502222222'); SELECT order_id, trim(p) AS productFROM sales_flat, string_to_table(products, ',') AS pORDER BY order_id;
النتيجة · 4 صفوف
order_idproduct
1حاسب محمول
1فأرة
2فأرة
3دفتر

توقّع وتحقّق

عمود فيه «فأرة, لوحة مفاتيح» يخالف:

رقم المورد في جدول الطلبات يعتمد على المورد لا على الطلب. هذا يخالف:

شذوذ الحذف يعني:

لنقل القيم الفريدة من جدول قديم إلى جديد نستخدم:

الخلاصة

تحدٍّ برمجي: فكّك جدول الموردين

متقدم

في الجدول sales_flat بيانات الموردين مكررة. أنشئ جدول suppliers واملأه من البيانات الموجودة، ثم أضف إلى sales_flat عمود supplier_id واملأه، واحذف عمودي المورد القديمين.

المطلوب:
  • suppliers (id serial PK, name text UNIQUE NOT NULL, phone text) مملوء بـ INSERT … SELECT DISTINCT.
  • عمود supplier_id في sales_flat يشير إلى suppliers (مفتاح أجنبي) ومملوء لكل الصفوف.
  • حذف العمودين supplier و supplier_phone من sales_flat.
👀 اعرض الناتج المطلوب
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء
  • مورّدان فقط في suppliers
  • كل صف في sales_flat يشير إلى مورده الصحيح
  • العمودان القديمان حُذفا
  • supplier_id مفتاح أجنبي (رقم مورد غير موجود يُرفض)

💡 تلميح 1
الملء من جدول آخر: UPDATE sales_flat f SET supplier_id = s.id FROM suppliers s WHERE s.name = f.supplier;
💡 تلميح 2
حذف عمودين بأمر واحد: ALTER TABLE t DROP COLUMN a, DROP COLUMN b;

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

solution.sql
CREATE TABLE sales_flat (  order_id      int,  order_date    date,  customer_name text,  customer_city text,  products      text,          -- قائمة مفصولة بفواصل!  supplier      text,  supplier_phone text);INSERT INTO sales_flat VALUES (1, '2025-09-01', 'سارة', 'الرياض', 'حاسب محمول, فأرة', 'مؤسسة النور', '0501111111'), (2, '2025-09-02', 'خالد', 'جدة',    'فأرة',             'مؤسسة النور', '0501111111'), (3, '2025-09-03', 'سارة', 'الرياض', 'دفتر',             'مكتبة الفكر', '0502222222'); CREATE TABLE suppliers (  id    serial PRIMARY KEY,  name  text NOT NULL UNIQUE,  phone text);INSERT INTO suppliers (name, phone)SELECT DISTINCT supplier, supplier_phone FROM sales_flat; ALTER TABLE sales_flat ADD COLUMN supplier_id int REFERENCES suppliers(id);UPDATE sales_flat f SET supplier_id = s.id FROM suppliers s WHERE s.name = f.supplier; ALTER TABLE sales_flat DROP COLUMN supplier, DROP COLUMN supplier_phone;
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس