التطبيع: من جدول واحد إلى جداول مترابطة
لماذا يفشل الجدول الواحد الكبير؟ وكيف نفكّكه بقواعد واضحة دون أن نخسر البيانات؟ ⏱ نحو 45 دقيقة.
- تكتشف التكرار وشذوذ الإدخال والتعديل والحذف في جدول غير مطبّع.
- تطبّق قواعد
1NFو2NFو3NF. - تفكّك جدولًا موجودًا وتنقل بياناته بـ
INSERT…SELECT DISTINCT. - تعرف متى يُقبل إلغاء التطبيع.
١جدول واحد لكل شيء؟
أسرع تصميم يخطر بالبال: جدول واحد فيه كل المعلومات. يبدو بسيطًا، لكنه يخفي مشكلات:
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;
| order_id | order_date | customer_name | customer_city | products | supplier | supplier_phone |
|---|---|---|---|---|---|---|
| 1 | 2025-09-01 | سارة | الرياض | حاسب محمول, فأرة | مؤسسة النور | 0501111111 |
| 2 | 2025-09-02 | خالد | جدة | فأرة | مؤسسة النور | 0501111111 |
| 3 | 2025-09-03 | سارة | الرياض | دفتر | مكتبة الفكر | 0502222222 |
| المشكلة | مثال من الجدول |
|---|---|
| التكرار | مدينة سارة ورقم «مؤسسة النور» مكتوبان أكثر من مرة |
| شذوذ التعديل | تغيّر رقم المورد ← يجب تعديله في كل صف، وإن نسيت صفًّا صار للمورد رقمان |
| شذوذ الإدخال | لا يمكن إضافة مورد جديد قبل أن يُطلب أحد منتجاته |
| شذوذ الحذف | حذف الطلب 3 يحذف معه الشيء الوحيد الذي نعرفه عن «مكتبة الفكر» |
| قيم مركّبة | عمود products فيه «حاسب محمول, فأرة»: لا نستطيع أن نسأل بسهولة «كم مرة بيعت الفأرة؟» |
٢الصيغ الطبيعية
التطبيع Normalization تفكيك الجدول إلى جداول أصغر بحيث تُحفظ كل معلومة في مكان واحد. ونقيسه بثلاث صيغ متدرجة:
| الصيغة | القاعدة | في مثالنا |
|---|---|---|
| 1NF الأولى | كل خلية قيمة واحدة ذرّية؛ لا قوائم ولا أعمدة مكررة | المنتجات تصبح صفوفًا في جدول البنود، لا نصًّا بفواصل |
| 2NF الثانية | 1NF + كل عمود يعتمد على كامل المفتاح المركّب لا جزء منه | اسم المنتج يعتمد على product_id وحده، فينتقل إلى products |
| 3NF الثالثة | 2NF + لا يعتمد عمود عادي على عمود عادي آخر | رقم المورد يعتمد على المورد لا على الطلب، فينتقل إلى suppliers |
٣التفكيك: من الجدول المسطّح إلى جداول مطبّعة
نقل البيانات الموجودة إلى الجداول الجديدة لا يحتاج كتابتها يدويًّا: INSERT … SELECT DISTINCT يأخذ القيم الفريدة من الجدول القديم.
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;
| id | name | phone |
|---|---|---|
| 1 | مؤسسة النور | 0501111111 |
| 2 | مكتبة الفكر | 0502222222 |
الآن رقم المورد محفوظ مرة واحدة. تعديله سطر واحد، وإضافة مورد جديد لا تحتاج طلبًا.
والقائمة «حاسب محمول, فأرة» تُفكّ إلى صفوف بالدالة string_to_table:
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;
| order_id | product |
|---|---|
| 1 | حاسب محمول |
| 1 | فأرة |
| 2 | فأرة |
| 3 | دفتر |
توقّع وتحقّق
عمود فيه «فأرة, لوحة مفاتيح» يخالف:
رقم المورد في جدول الطلبات يعتمد على المورد لا على الطلب. هذا يخالف:
شذوذ الحذف يعني:
لنقل القيم الفريدة من جدول قديم إلى جديد نستخدم:
الخلاصة
- الجدول غير المطبّع يكرر البيانات ويسبب شذوذ الإدخال والتعديل والحذف.
1NF: قيم ذرّية.2NF: الاعتماد على كامل المفتاح.3NF: لا اعتماد بين الأعمدة العادية.- كل عمود يعتمد على المفتاح، والمفتاح كله، ولا شيء غير المفتاح.
INSERT…SELECT DISTINCTثمUPDATE…FROMلنقل البيانات إلى التصميم الجديد.
تحدٍّ برمجي: فكّك جدول الموردين
متقدمفي الجدول 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;حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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;