SQL: عمليات CRUD
درس 1 من 5
1SQL: عمليات CRUD (CRUD Operations)
الصورة الكبيرةمن إنشاء أول جدول، إلى إضافة البيانات وقراءتها وتعديلها وحذفها وتلخيصها بالدوال التجميعية، على جدول واحد، قبل أن يدخل الربط بين الجداول في الصورة.
ماذا تشاهد في الرسم: INSERT تضيف صف الشاي، وSELECT تضيء ما سعره أقل من 10، وUPDATE تغيّر سعر اللاتيه، وDELETE تحذف الموكا.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
قاعدة البيانات مثل جدول بيانات منظّم جدًا في مكتب. تستطيع أن تفعل به أربعة أشياء فقط: تضيف صفًّا، تقرأ صفوفًا، تعدّل صفًّا، تحذف صفًّا. وSQL هي اللغة التي تكتب بها طلبك لأمين الجدول: «أعطني أسماء الطلاب الذين درجتهم فوق 90، مرتّبين من الأعلى».
CREATE TABLE يعرّف الأعمدة وأنواعها وقيودها مثل PRIMARY KEY وNOT NULL. ثم INSERT وSELECT وUPDATE وDELETE (CRUD)، مع WHERE للتصفية وORDER BY وLIMIT. والدوال التجميعية COUNT وSUM وAVG مع GROUP BY تلخّص الصفوف، وNULL يعني «غير معروف» ويُفحص بـIS NULL.
الأساسيات المطلوبة
SQL مجال جديد، لكن ثلاثة أشياء مما سبق تجعل القفزة أسهل:
الجدول: صفوف وأعمدة
الجدول (Table) يمثّل نوعًا واحدًا من الأشياء (مشروبات، طلاب). كل صف سجل واحد، وكل عمود خاصية بنوع محدد. إن استخدمت Excel أو Sheets فالفكرة مألوفة، الفرق أن أعمدة قاعدة البيانات لها قواعد صارمة تحميها.
القاموس والتسلسل (درسا هياكل البيانات والتسلسل)
الصف يشبه قاموسًا واحدًا: مفاتيح ثابتة (أسماء الأعمدة) وقيم تتغيّر من صف لآخر. وفرقه عن التسلسل أن Pickle/JSON يعيد كتابة الملف كله، بينما قاعدة البيانات تغيّر صفًا واحدًا وتحضر عن أسئلة مثل «أغلى مشروب بارد» دون قراءة كل شيء إلى الذاكرة.
الاستثناءات والملفات (درسا الاستثناءات والملفات)
قاعدة SQLite ملف واحد على القرص. وانتهاك قيد (قيمة مكرّرة أو فارغة أو خارج النطاق) يُرجع خطأ؛ وفي بايثون يظهر كاستثناء sqlite3.IntegrityError تلتقطه بـ try/except كما تعلّمت.
المفاهيم الأساسية
SQL لغة وصفية: تقول ماذا تريد من البيانات لا كيف تحصل عليها خطوة بخطوة. والأهم: هي تعمل على مجموعات من الصفوف دفعة واحدة، لا على صف بعد صف كما تفعل حلقة for في بايثون.
1الأوامر الأربعة
INSERT: Create
يضيف صفًا أو عدة صفوف: INSERT INTO t (أعمدة) VALUES (…), (…). الأعمدة غير المذكورة تأخذ DEFAULT أو NULL. وتوجد صيغة INSERT … SELECT تنسخ نتيجة استعلام إلى جدول.
SELECT: Read
يقرأ دون أن يعدّل شيئًا: تختار الأعمدة (SELECT name, price_halalas)، تصفّي الصفوف بـ WHERE، ترتّب بـ ORDER BY، وتقصّ بـ LIMIT.
UPDATE: Update
UPDATE t SET عمود = تعبير [, …] WHERE …. تُحسب كل التعبيرات على القيم القديمة للصف قبل أي تعيين، لذلك SET a = b, b = a تبدّل القيمتين فعلًا.
DELETE: Delete
DELETE FROM t WHERE … يحذف الصفوف التي شرطها صحيح. الصفوف التي شرطها خطأ أو NULL تبقى كما هي، والجدول نفسه يبقى (بعكس DROP TABLE).
UPDATE أو DELETE بلا WHERE. هذا صحيح تمامًا من ناحية SQL فيُطبَّق على كل صف في الجدول بلا تحذير ولا تأكيد. العادة الذهبية: اكتب SELECT بنفس شرط WHERE أولًا وراجع الصفوف، ثم بدّلها بالأمر الفعلي.
2الشروط: كيف تختار الصفوف
| العامل | المعنى | مثال |
|---|---|---|
= / <> / < / >= | مقارنة عادية | WHERE price_halalas >= 1800 |
AND / OR / NOT | دمج الشروط، AND يسبق OR فاستخدم الأقواس | WHERE category = 'cold' AND (stock < 15 OR price_halalas > 2000) |
IN (…) / BETWEEN a AND b | عضوية في قائمة / نطاق شامل الطرفين | WHERE name IN ('Cortado', 'Matcha') |
LIKE | نمط نصي: % أي عدد من الحروف، _ حرف واحد | WHERE name LIKE 'C%' |
IS NULL / IS NOT NULL | الطريقة الوحيدة الصحيحة لفحص الفراغ | WHERE note IS NULL |
3الترتيب والحدّ
بدون ORDER BY يكون ترتيب الصفوف غير مضمون حتى لو بدا ثابتًا اليوم. ORDER BY price_halalas DESC, name يرتّب تنازليًا بالسعر ثم أبجديًا عند التعادل. و LIMIT 3 يأخذ أول ثلاثة صفوف بعد الترتيب؛ LIMIT 3 OFFSET 3 يتخطى الثلاثة الأولى. في SQLite تُعامَل NULL كأنها أصغر من أي قيمة أخرى عند الترتيب.
4الدوال التجميعية و GROUP BY
الدالة التجميعية تختصر عدة صفوف في قيمة واحدة. مع GROUP BY تحصل على صف ناتج واحد لكل مجموعة.
| الدالة | ماذا تحسب |
|---|---|
COUNT(*) | عدد الصفوف كلها |
COUNT(col) | عدد الصفوف التي قيمة col فيها ليست NULL |
SUM(col) / AVG(col) | المجموع / المتوسط، يتجاهلان NULL |
MIN(col) / MAX(col) | أصغر / أكبر قيمة |
WHERE يصفّي الصفوف قبل التجميع، و HAVING يصفّي المجموعات بعده. لذلك لا يمكنك كتابة WHERE COUNT(*) > 2، سيرفضها SQLite بخطأ misuse of aggregate، بل HAVING COUNT(*) > 2.
SELECT category, name, COUNT(*) … GROUP BY category أي name يجب أن تعرض ما دامت المجموعة فيها عدة أسماء؟ المعايير الصارمة تمنع هذا الاستعلام، أما SQLite فتسمح به وتختار قيمة عشوائية عمليًا من المجموعة. لا تعتمد على هذا السلوك: اجعل كل عمود في SELECT إما داخل GROUP BY أو داخل دالة تجميعية.
5القيود و NULL والأنواع
| القيد | ماذا يفعل | مثال |
|---|---|---|
PRIMARY KEY | يُعرّف كل صف بتفرّد. العمود INTEGER PRIMARY KEY يتولّد تلقائيًا إن تركته. | id INTEGER PRIMARY KEY |
NOT NULL | يرفض ترك الخانة فارغة. | name TEXT NOT NULL |
UNIQUE | يرفض تكرار القيمة في صفّين. | name TEXT UNIQUE |
CHECK | يرفض أي قيمة لا يحقق تعبيرها الشرط. | CHECK (price_halalas > 0) |
DEFAULT | قيمة تُستخدم حين لا تذكر العمود في INSERT. | stock INTEGER DEFAULT 0 |
NULL تعني «لا توجد قيمة / غير معروف»، وليست صفرًا ولا نصًّا فارغًا. أي مقارنة معها تنتج NULL لا «صح»: NULL = NULL ليست صحيحة، ولهذا نكتب IS NULL. وتتجاهل الدوال التجميعية NULL ما عدا COUNT(*).
وفي SQLite الأنواع مرنة: النوع المعلن للعمود هو «تفضيل» (Type Affinity) لا قيد صارم، فقد يُخزَّن نص داخل عمود INTEGER إن تعذّر تحويله. الجداول المعلنة بـ STRICT (منذ SQLite 3.37.0) تفرض الأنواع بصرامة، لكن لا تفترض توفّرها في بيئة التصحيح عندك. اكتب دائمًا SQL صحيحًا منطقيًا ولا تتكل على تساهل المحرّك.
كيف تعمل هذه الأوامر فعليًا؟
دورة العمل الآمن مع التعديل والحذف
الرسم 21: SELECT هو «معاينة» الأمر الخطِر: ما يرجعه هو بالضبط ما سيتأثر.
الترتيب المنطقي لتنفيذ SELECT
الرسم 22: نكتب SELECT أولًا لكنه يُنفَّذ منطقيًا قبل الأخيرين: لهذا لا يرى WHERE نتيجة الدوال التجميعية.
ماذا يحدث داخل UPDATE؟
- يختار المحرّك الصفوف التي يكون شرط
WHEREفيها صحيحًا فقط (الخطأ و NULL يُستبعدان). - لكل صف مختار يحسب كل تعبيرات
SETعلى القيم القديمة. - يفحص القيود (
NOT NULL،UNIQUE،CHECK…) على الصف الجديد. - عند أي انتهاك يُلغى الأمر كله (السلوك الافتراضي
ABORT) فلا يتغيّر شيء، وإلا تُحفَظ التغييرات.
مثال تطبيقي كامل: قائمة مشروبات مقهى
سنبني جدولًا واحدًا لمشروبات مقهى ونمرّ به على كل العمليات. المجال مختلف تمامًا عن بيانات مشروعك حتى تطبّق الأسلوب نفسه بنفسك لاحقًا.
الخطوة 1: إنشاء الجدول بقيود تحميه
CREATE TABLE drinks (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
category TEXT NOT NULL CHECK (category IN ('hot', 'cold')),
price_halalas INTEGER NOT NULL CHECK (price_halalas > 0),
stock INTEGER NOT NULL DEFAULT 0,
note TEXT
);
السعر مخزَّن بالهللات (عدد صحيح) لا بالريال العشري: الأعداد العشرية الثنائية قد تُدخل أخطاء تقريب في المبالغ المالية.
الخطوة 2: الإضافة: صف واحد، عدة صفوف، وأعمدة محذوفة
INSERT INTO drinks (name, category, price_halalas, stock)
VALUES ('Flat White', 'hot', 1600, 40);
INSERT INTO drinks (name, category, price_halalas, stock, note)
VALUES ('Cold Brew', 'cold', 1800, 25, '12h steeped'),
('Spanish Latte', 'cold', 1950, 18, NULL),
('Cortado', 'hot', 1400, 30, NULL),
('Matcha', 'cold', 2100, 12, 'ceremonial grade');
INSERT INTO drinks (name, category, price_halalas)
VALUES ('Americano', 'hot', 1200); -- stock omitted -> DEFAULT 0, note omitted -> NULL
النتيجة: SELECT * FROM drinks ORDER BY id;
| id | name | category | price_halalas | stock | note |
|---|---|---|---|---|---|
| 1 | Flat White | hot | 1600 | 40 | NULL |
| 2 | Cold Brew | cold | 1800 | 25 | 12h steeped |
| 3 | Spanish Latte | cold | 1950 | 18 | NULL |
| 4 | Cortado | hot | 1400 | 30 | NULL |
| 5 | Matcha | cold | 2100 | 12 | ceremonial grade |
| 6 | Americano | hot | 1200 | 0 | NULL |
لاحظ Americano: لم نذكر stock فأخذ 0 (الافتراضي)، ولم نذكر note فأخذ NULL. وعمود id تولّد وحده.
الخطوة 3: القراءة: شرط، ترتيب، وحدّ
SELECT name, price_halalas FROM drinks WHERE category = 'cold' AND stock >= 15 ORDER BY price_halalas DESC;
| name | price_halalas |
|---|---|
| Spanish Latte | 1950 |
| Cold Brew | 1800 |
SELECT name, price_halalas FROM drinks ORDER BY price_halalas DESC LIMIT 3;
| name | price_halalas |
|---|---|
| Matcha | 2100 |
| Spanish Latte | 1950 |
| Cold Brew | 1800 |
Matcha مستبعد من الاستعلام الأول لأن مخزونه 12 فقط. والاستعلام الثاني يرتّب أولًا ثم يقصّ، لو غيّرت الترتيب لتغيّرت الصفوف الثلاثة.
الخطوة 4: التعديل: معاينة ثم تنفيذ
SELECT id, name, price_halalas FROM drinks WHERE category = 'cold';
| id | name | price_halalas |
|---|---|---|
| 2 | Cold Brew | 1800 |
| 3 | Spanish Latte | 1950 |
| 5 | Matcha | 2100 |
UPDATE drinks SET price_halalas = price_halalas + 100 WHERE category = 'cold'; -- 3 rows changed
| id | name | price_halalas |
|---|---|---|
| 2 | Cold Brew | 1900 |
| 3 | Spanish Latte | 2050 |
| 5 | Matcha | 2200 |
price + 100 يعتمد على الحالة الحالية، أما SET price_halalas = 1900 فيعطي النتيجة نفسها مهما كرّرته.
الخطوة 5: الحذف: عاين أولًا
SELECT id, name, stock FROM drinks WHERE stock = 0;
| id | name | stock |
|---|---|---|
| 6 | Americano | 0 |
DELETE FROM drinks WHERE stock = 0; -- 1 row removed
في أنظمة حقيقية كثيرة لا يُحذف الصف فعليًا بل يُعلَّم بعمود مثل is_active = 0 (حذف ناعم Soft Delete) حتى يمكن استرجاعه أو تتبّعه؛ الحذف الفعلي (Hard Delete) لا رجعة فيه.
الخطوة 6: التلخيص: الدوال التجميعية و GROUP BY
SELECT category,
COUNT(*) AS n,
AVG(price_halalas) AS avg_price,
MAX(price_halalas) AS max_price,
SUM(stock) AS total_stock
FROM drinks
GROUP BY category
ORDER BY category;
| category | n | avg_price | max_price | total_stock |
|---|---|---|---|---|
| cold | 3 | 2050.0 | 2200 | 55 |
| hot | 2 | 1500.0 | 1600 | 70 |
SELECT COUNT(*) AS all_rows, COUNT(note) AS with_note FROM drinks;
| all_rows | with_note |
|---|---|
| 5 | 2 |
COUNT(*) عدّ الصفوف الخمسة، أما COUNT(note) فعدّ اثنين فقط لأن بقية الملاحظات NULL.
SELECT category, COUNT(*) AS n FROM drinks GROUP BY category HAVING COUNT(*) > 2;
| category | n |
|---|---|
| cold | 3 |
الخطوة 7: ماذا تقول القيود حين تُنتهك؟
| المحاولة | رسالة الخطأ في SQLite |
|---|---|
INSERT … VALUES ('Mocha', 'warm', 1500) | CHECK constraint failed: category IN ('hot', 'cold') |
INSERT … VALUES ('Cortado', 'hot', 1500) | UNIQUE constraint failed: drinks.name |
INSERT … VALUES ('X', 'hot', 0) | CHECK constraint failed: price_halalas > 0 |
INSERT INTO drinks (name, category) VALUES ('X', 'hot') | NOT NULL constraint failed: drinks.price_halalas |
الخطوة 8: من بايثون
import sqlite3
conn = sqlite3.connect("cafe.db")
try:
with conn: # يؤكّد (commit) عند النجاح ويتراجع (rollback) عند أي استثناء
cur = conn.execute(
"UPDATE drinks SET price_halalas = ? WHERE name = ?",
(1700, "Flat White"), # ؟ = مكان القيمة، لا تدمج النص بنفسك أبدًا
)
print("rows changed:", cur.rowcount) # 1
except sqlite3.IntegrityError as err:
print("rejected:", err) # مثال: CHECK constraint failed
finally:
conn.close()
? وسلّم القيم منفصلة كما في المثال، فيتعامل المحرّك معها كبيانات لا كشيفرة.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
DELETE FROM drinks يحذف الجدول | يحذف كل الصفوف فقط؛ بنية الجدول تبقى. حذف الجدول نفسه يتطلب DROP TABLE. |
WHERE note = NULL يجد الصفوف الفارغة | لا يجد شيئًا أبدًا لأن المقارنة مع NULL تنتج NULL لا «صح». الصحيح WHERE note IS NULL. |
| SELECT بلا ORDER BY يرجع الصفوف بترتيب إدخالها دائمًا | الترتيب غير مضمون؛ قد يتغيّر بعد تعديل أو فهرسة. أي نتيجة يهم ترتيبها تحتاج ORDER BY صريحًا. |
| SQLite ترفض نصًا داخل عمود INTEGER | ليس بالضرورة: الأنواع مرنة، فإن تعذّر تحويل 'abc' إلى رقم خُزِّن نصًا كما هو. (جداول STRICT فقط تمنع ذلك.) |
COUNT(note) و COUNT(*) نفس الشيء | COUNT(*) يعدّ كل الصفوف، و COUNT(note) يتجاهل الصفوف التي note فيها NULL. |
أين تُستخدم هذه الأدوات فعليًا؟
سلّة التسوق
«أضف للسلة» = INSERT، «غيّر الكمية» = UPDATE، «احذف المنتج» = DELETE، وعرض السلة = SELECT، كلها عمليات CRUD على جدول واحد.
لوحات المؤشرات
«عدد الطلبات اليوم»، «متوسط السعر لكل فئة»، «أكثر المنتجات مخزونًا» كلها COUNT/AVG/MAX مع GROUP BY.
تنظيف البيانات
إصلاح أخطاء إدخال جماعية بـ UPDATE … WHERE مدروس، بعد معاينة الصفوف المتأثرة بـ SELECT.
تطبيقات الجوال المحلية
SQLite مدمجة في الهواتف والمتصفحات وكثير من التطبيقات لحفظ بيانات محلية بلا خادم.
جرّب بنفسك
قبل DELETE FROM drinks WHERE price > 20، ما الخطوة الآمنة؟
SELECT بالشرط نفسه يريك بالضبط الصفوف التي ستُحذف. الحذف بلا معاملة مفتوحة لا رجعة فيه.
بطاقات مراجعة سريعة
UPDATE drinks SET stock = 0; بلا WHERE؟WHERE ثم HAVING.COUNT(*) و COUNT(col)؟col فيها NULL.NULL = NULL؟ وماذا نستخدم بدلًا منها؟IS NULL.اختبر فهمك
1. (استرجاع) سمِّ حروف CRUD الأربعة وأمر SQL المقابل لكل حرف.
Create → INSERT، Read → SELECT، Update → UPDATE، Delete → DELETE.
2. (لماذا) لماذا لا يُعدّ UPDATE drinks SET price_halalas = price_halalas + 100 WHERE category = 'cold' آمنًا للتكرار؟
لأن التعبير يعتمد على القيمة الحالية، فكل تشغيل جديد يضيف 100 أخرى. تعيين قيمة ثابتة يعطي النتيجة نفسها مهما كرّرته.
3. (اكتشف الخطأ) ما المشكلة في UPDATE drinks SET price_halalas = 2000 AND stock = 5 WHERE id = 3;؟
استُخدمت AND مكان الفاصلة. SQLite تقيّم 2000 AND stock = 5 كتعبير منطقي فتصبح قيمة السعر 1 أو 0 لا 2000 (وإن كانت 0 فسيرفضها قيد CHECK). الصحيح: SET price_halalas = 2000, stock = 5.
4. (اكتشف الخطأ) لماذا يفشل SELECT category, COUNT(*) FROM drinks WHERE COUNT(*) > 2 GROUP BY category; وكيف تصلحه؟
لأن WHERE يعمل قبل التجميع فلا يرى نتيجة COUNT (خطأ misuse of aggregate). الحل: انقل الشرط إلى HAVING COUNT(*) > 2 بعد GROUP BY.
5. (توقّع) في جدولنا بعد الحذف، ما قيمة SELECT COUNT(note) FROM drinks;؟
2، فقط Cold Brew وMatcha لهما ملاحظة؛ بقية الصفوف NULL فلا تُعدّ.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.