دليل شامل لمعاملات SQL: استكشاف معاملات البت، المقارنة، الحسابية، والمنطقية
مقدمة إلى معاملات SQL: مفتاح تصفية البيانات
في جوهرها، لا تعد الإنترنت وتطبيقاتها سوى مجموعة هائلة من البيانات. كل بريد إلكتروني، تغريدة، صورة شخصية، معاملة بنكية، وغيرها، هي مجرد بيانات مخزنة في قاعدة بيانات ما. لكي تكون هذه البيانات ذات قيمة، يجب أن نكون قادرين على استرجاعها. ومع ذلك، لا يكفي مجرد استرجاع البيانات؛ بل يجب أن تكون مفيدة وذات صلة بسياقنا. على مستوى قواعد البيانات، نطلب معلومات محددة عن طريق كتابة استعلامات SQL. تحدد استعلامات SQL هذه البيانات التي نرغب في الحصول عليها والشكل الذي نريدها به. في هذا المقال، سنتعمق في استكشاف الطرق الأكثر شيوعًا لتصفية استعلامات SQL. إليك ما سنتناوله:
- إعداد قاعدة البيانات الخاصة بك
- إنشاء المستخدمين
- إدراج المستخدمين
- تصفية البيانات باستخدام جملة
WHERE - المعاملات المنطقية (
AND/OR/NOT) - معاملات المقارنة (
<،>،<=،>=) - المعاملات الحسابية (
+،-،*،/،%) - معاملات الوجود (
IN/NOT IN) - المطابقة الجزئية باستخدام
LIKE - التعامل مع البيانات المفقودة (
NULL) - استخدام
IS NULLوIS NOT NULL - معاملات المقارنة مع التواريخ والأوقات
- التحقق من الوجود باستخدام
EXISTS/NOT EXISTS - معاملات البت (
Bitwise Operators)
إعداد قاعدة البيانات
للبدء في تصفية البيانات، يجب أن نمتلك بيانات أولاً. في هذه الأمثلة، سنعتمد على نظام قواعد البيانات PostgreSQL، لكن المفاهيم والاستعلامات التي سنعرضها قابلة للتطبيق بسهولة على أي نظام قواعد بيانات حديث آخر (مثل MySQL، SQL Server، وما إلى ذلك). للتعامل مع قاعدة بيانات PostgreSQL الخاصة بنا، يمكننا استخدام psql، وهو برنامج سطر الأوامر التفاعلي لـ PostgreSQL. إذا كنت تفضل استخدام عميل قاعدة بيانات آخر، فلا بأس بذلك! لنبدأ بإنشاء قاعدة بياناتنا. بعد تثبيت PostgreSQL، يمكننا تشغيل الأمر createdb <database-name> في طرفيتنا لإنشاء قاعدة بيانات جديدة. لقد أطلقت على قاعدتي اسم fcc:
$ createdb fcc
بعد ذلك، لنبدأ تشغيل وحدة التحكم التفاعلية باستخدام الأمر psql، ثم نتصل بقاعدة البيانات التي أنشأناها للتو باستخدام \c <database-name>:
$ psql
psql (11.5)
Type "help" for help.
john=# \c fcc
You are now connected to database "fcc" as user "john".
fcc=#
إنشاء المستخدمين (Users)
بعد أن قمنا بإعداد قاعدة البيانات، حان الوقت لإنشاء جدول لنمذجة المستخدمين المحتملين في نظامنا الافتراضي. سنطلق على هذا الجدول اسم users، وسيمثل كل صف فيه مستخدمًا واحدًا من مستخدمينا. سيحتوي جدول users على أعمدة نتوقع أن تصف المستخدم، مثل الاسم والبريد الإلكتروني والعمر. داخل جلسة psql الخاصة بنا، لنقم بإنشاء جدول users:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
age INTEGER NOT NULL
);
يُظهر الإخراج النص CREATE TABLE، مما يعني أن عملية إنشاء الجدول قد نجحت. ملاحظة: لقد قمت بتبسيط إخراج psql في هذه الأمثلة لتسهيل القراءة، لذا لا تقلق إذا لم يكن الإخراج المعروض هنا مطابقًا تمامًا لما تراه في طرفيتك. لنلقِ نظرة على محتويات جدول المستخدمين الخاص بنا:
SELECT * FROM users;
لم نقم بإدراج أي بيانات في جدولنا بعد، لذا نرى فقط هيكل الجدول الفارغ. إذا لم تكن على دراية باستعلامات SQL، فإن الاستعلام الذي قمنا بتشغيله للتو، SELECT * FROM users، هو أحد أبسط الاستعلامات التي يمكنك كتابتها. تحدد الكلمة المفتاحية SELECT الأعمدة التي ترغب في استرجاعها (تشير * إلى “جميع الأعمدة”)، بينما تحدد الكلمة المفتاحية FROM الجدول الذي تريد الاستعلام منه (في هذه الحالة، جدول users). وبالتالي، فإن SELECT * FROM users تعني فعليًا استرجاع جميع الصفوف وجميع الأعمدة من جدول users. إذا أردنا استرجاع أعمدة محددة من جدول users، يمكننا استبدال SELECT * بأسماء الأعمدة التي نرغب في استرجاعها، على سبيل المثال SELECT id, name FROM users.
إدراج المستخدمين (Inserting Users)
الجدول الفارغ ليس مفيدًا جدًا، لذا دعنا نُدرج بعض البيانات فيه حتى نتمكن من التدرب على الاستعلامات:
INSERT INTO users (first_name, last_name, email, age) VALUES
('John', 'Smith', 'johnsmith@gmail.com', 25),
('Jane', 'Doe', 'janedoe@Gmail.com', 28),
('Xavier', 'Wills', 'xavier@wills.io', 35),
('Bev', 'Scott', 'bev@bevscott.com', 16),
('Bree', 'Jensen', 'bjensen@corp.net', 42),
('John', 'Jacobs', 'jjacobs@corp.net', 56),
('Rick', 'Fuller', 'fullman@hotmail.com', 16);
إذا قمنا بتشغيل أمر الإدراج هذا في جلسة psql الخاصة بنا، فسنرى الإخراج INSERT 0 7. هذا يعني أننا أدرجنا بنجاح 7 صفوف جديدة في جدولنا. إذا قمنا بتشغيل استعلام SELECT * FROM users مرة أخرى، فسنرى الآن هذه البيانات:
SELECT * FROM users;
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
| 2 | Rick | Fuller | fullman@hotmail.com | 16 |
| 3 | Bree | Jensen | bjensen@corp.net | 42 |
| 4 | Bev | Scott | bev@bevscott.com | 16 |
| 5 | Xavier | Wills | xavier@wills.io | 35 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
| 7 | John | Smith | johnsmith@gmail.com | 25 |
تصفية البيانات باستخدام جملة WHERE
حتى الآن، قمنا فقط باسترجاع جميع الصفوف من جدولنا، وهذا هو السلوك الافتراضي للاستعلام. لاسترجاع مجموعة أكثر تحديدًا من الصفوف، نحتاج إلى تصفية هذه الصفوف باستخدام جملة WHERE. هناك العديد من الطرق لتصفية الصفوف باستخدام جملة WHERE. أبسط معامل يمكننا استخدامه هو معامل المساواة: =. لنفترض أننا أردنا العثور على المستخدمين الذين اسمهم الأول “John”:
SELECT * FROM users WHERE first_name = 'John';
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
| 7 | John | Smith | johnsmith@gmail.com | 25 |
هنا، أضفنا الكلمة المفتاحية WHERE إلى استعلامنا متبوعة بعبارة مساواة: first_name = 'John'. تنظر قاعدة البيانات أولاً إلى الكلمة المفتاحية FROM لتحديد البيانات التي ستجلبها. لذا، ستقرأ قاعدة البيانات هذا الاستعلام، وترى FROM users، ثم تذهب لجلب جميع الصفوف لجدول users من القرص. بمجرد استرجاع جميع الصفوف من جدول users، تقوم بعد ذلك بتطبيق جملة WHERE على كل صف وتعيد فقط الصفوف التي تكون فيها قيمة عمود first_name مساوية لـ “John”. في بياناتنا، هناك صفان يتطابقان مع هذا الاسم الأول. إذا أردنا العثور على “John” معين في نظامنا، يمكننا الاستعلام بناءً على عمود نعرف أنه فريد — مثل عمود id الخاص بنا. للعثور على صف “John Jacobs” تحديدًا، يمكننا الاستعلام باستخدام معرفه:
SELECT * FROM users WHERE id = 1;
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
هنا، تطابق سجل واحد فقط مع الشرط id = 1، لذلك استرجعنا صفًا واحدًا فقط.
المعاملات المنطقية (AND / OR / NOT)
يمكننا التصفية بأكثر من مجرد معامل المساواة. يمكننا أيضًا استخدام المعاملات المنطقية البوليانية الموجودة في معظم لغات البرمجة: AND (و)، OR (أو)، و NOT (ليس). في العديد من لغات البرمجة، يتم تمثيل AND و OR بالرمزين && و || على التوالي. أما في SQL، فهي ببساطة AND و OR. بدلاً من الاستعلام بواسطة المعرف (ID)، دعنا نحاول العثور على سجل الشخص الذي يُدعى “John Smith”. للقيام بذلك، يمكننا استخدام المعامل AND في جملة WHERE للبحث عن شرطي الاسم الأول واسم العائلة معًا:
SELECT * FROM users WHERE first_name = 'John' AND last_name = 'Smith';
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 7 | John | Smith | johnsmith@gmail.com | 25 |
للعثور على الأشخاص الذين اسمهم الأول “John” أو اسم عائلتهم “Doe”:
SELECT * FROM users WHERE first_name = 'John' OR last_name = 'Doe';
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
| 7 | John | Smith | johnsmith@gmail.com | 25 |
هنا، احتوت نتيجتنا على كل من “Johns” و “Jane Doe”. يمكن أيضًا ربط شروط AND و OR معًا. لنفترض أننا أردنا العثور على شخص اسمه “John Smith” بالضبط، أو شخص يحمل اسم عائلة “Doe”:
SELECT * FROM users WHERE (first_name = 'John' AND last_name = 'Smith') OR last_name = 'Doe';
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
| 7 | John | Smith | johnsmith@gmail.com | 25 |
إذا أردنا عكس هذا الشرط والعثور على المستخدمين الذين ليس اسمهم “John Smith” وليس اسم عائلتهم “Doe”، يمكننا إضافة المعامل NOT:
SELECT * FROM users WHERE NOT ((first_name = 'John' AND last_name = 'Smith') OR last_name = 'Doe');
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 4 | Bev | Scott | bev@bevscott.com | 16 |
| 5 | Bree | Jensen | bjensen@corp.net | 42 |
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
| 2 | Rick | Fuller | fullman@hotmail.com | 16 |
| 3 | Xavier | Wills | xavier@wills.io | 35 |
ملاحظة: لكل مبرمج أسلوبه الخاص في تنسيق الاستعلامات، لذا استخدم الأسلوب الذي تراه أكثر وضوحًا وملاءمة لك!
معاملات المقارنة (< ، > ، <= ، >=)
على غرار لغات البرمجة الأخرى، تدعم SQL أيضًا معاملات المقارنة التالية: < (أقل من)، > (أكبر من)، <= (أقل من أو يساوي)، و >= (أكبر من أو يساوي). لنطبق هذه المعاملات على عمود age (العمر) الخاص بمستخدمينا. لنفترض أننا أردنا العثور على المستخدمين الذين تبلغ أعمارهم ثمانية عشر عامًا أو أكثر:
SELECT * FROM users WHERE age >= 18;
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
| 3 | Bree | Jensen | bjensen@corp.net | 42 |
| 5 | Xavier | Wills | xavier@wills.io | 35 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
| 7 | John | Smith | johnsmith@gmail.com | 25 |
ماذا عن المستخدمين الذين تزيد أعمارهم عن 25 عامًا، ولكن تقل عن أو تساوي 35 عامًا؟
SELECT * FROM users WHERE age > 25 AND age <= 35;
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 5 | Xavier | Wills | xavier@wills.io | 35 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
المعاملات الحسابية (+ ، - ، * ، / ، %)
يمكننا أيضًا إجراء عمليات حسابية على بياناتنا. يحتوي جدول users الخاص بنا على عمود age (العمر)، فماذا لو أردنا إيجاد نصف عمر كل شخص؟
SELECT *, age / 2 AS half_of_their_age FROM users;
| id | first_name | last_name | age | half_of_their_age | |
|---|---|---|---|---|---|
| 1 | John | Jacobs | jjacobs@corp.net | 56 | 28 |
| 2 | Rick | Fuller | fullman@hotmail.com | 16 | 8 |
| 3 | Bree | Jensen | bjensen@corp.net | 42 | 21 |
| 4 | Bev | Scott | bev@bevscott.com | 16 | 8 |
| 5 | Xavier | Wills | xavier@wills.io | 35 | 17 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 | 14 |
| 7 | John | Smith | johnsmith@gmail.com | 25 | 12 |
هنا، نختار جميع أعمدة الجدول (باستخدام SELECT *)، ونقوم أيضًا باختيار عملية حسابية تجميعية جديدة: age / 2. كما نمنح هذه القيمة اسمًا وصفيًا (half_of_their_age) باستخدام الكلمة المفتاحية AS. يمكننا أيضًا العثور على من كان عمره عددًا زوجيًا باستخدام معامل باقي القسمة (%):
SELECT * FROM users WHERE (age % 2) = 0;
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
| 2 | Rick | Fuller | fullman@hotmail.com | 16 |
| 3 | Bree | Jensen | bjensen@corp.net | 42 |
| 4 | Bev | Scott | bev@bevscott.com | 16 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
يمكننا العثور على من كان عمره عددًا فرديًا بتغيير شرط المساواة = إلى “لا يساوي” باستخدام != أو <>:
SELECT * FROM users WHERE (age % 2) <> 0;
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 5 | Xavier | Wills | xavier@wills.io | 35 |
| 7 | John | Smith | johnsmith@gmail.com | 25 |
معاملات الوجود (IN / NOT IN)
إذا أردنا التحقق مما إذا كانت قيمة عمود معينة موجودة ضمن قائمة من القيم، يمكننا استخدام المعاملين IN أو NOT IN:
SELECT * FROM users WHERE first_name IN ('John', 'Jane', 'Rick');
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 7 | John | Smith | johnsmith@gmail.com | 25 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
| 2 | Rick | Fuller | fullman@hotmail.com | 16 |
وبالمثل، يمكننا استخدام NOT IN لنفي هذا الشرط:
SELECT * FROM users WHERE first_name NOT IN ('John', 'Jane', 'Rick');
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 3 | Xavier | Wills | xavier@wills.io | 35 |
| 4 | Bev | Scott | bev@bevscott.com | 16 |
| 5 | Bree | Jensen | bjensen@corp.net | 42 |
المطابقة الجزئية باستخدام LIKE
في بعض الأحيان، قد نرغب في البحث عن صفوف بناءً على مطابقة جزئية. على سبيل المثال، إذا أردنا العثور على جميع المستخدمين الذين قاموا بالتسجيل في تطبيقنا باستخدام عنوان بريد إلكتروني من Gmail. يمكننا إجراء مطابقة جزئية مقابل عمود باستخدام الكلمة المفتاحية LIKE. يمكننا أيضًا تحديد حرف بدل (أو “مطابقة أي شيء”) في سلسلة المطابقة باستخدام %. للعثور على المستخدمين الذين ينتهي بريدهم الإلكتروني بـ gmail.com:
SELECT * FROM users WHERE email LIKE '%gmail.com';
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 7 | John | Smith | johnsmith@gmail.com | 25 |
السلسلة %gmail.com تعني “مطابقة أي شيء ينتهي بـ gmail.com“. إذا عدنا إلى بيانات المستخدمين لدينا، سنلاحظ أن لدينا في الواقع مستخدمين اثنين لديهما عنوان بريد إلكتروني ينتهي بـ gmail.com:
('John', 'Smith', 'johnsmith@gmail.com', 25),
('Jane', 'Doe', 'janedoe@Gmail.com', 28),
ومع ذلك، فإن بريد “Jane” الإلكتروني يحتوي على حرف “G” كبير في عنوان بريدها الإلكتروني. لم يلتقط استعلامنا السابق هذا السجل لأنه كان يطابق بالضبط gmail.com بحرف “g” صغير. لإجراء مطابقة غير حساسة لحالة الأحرف، نحتاج فقط إلى استبدال LIKE بـ ILIKE:
SELECT * FROM users WHERE email ILIKE '%gmail.com';
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 7 | John | Smith | johnsmith@gmail.com | 25 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
يشير رمز البدل % في بداية السلسلة إلى أن أي شيء ينتهي بـ “gmail.com” سيتم إرجاعه. يمكن أن يكون ذلك bob.jones+12345@gmail.com أو asdflkasdflkj@gmail.com — طالما أنه ينتهي بـ gmail.com. يمكننا أيضًا إضافة أي عدد نريده من رموز البدل (%). على سبيل المثال، سيعيد مصطلح البحث %j%o% أي بريد إلكتروني يتبع النمط: <أي شيء> متبوعًا بحرف j، ثم <أي شيء>، ثم حرف o، ثم <أي شيء>:
SELECT * FROM users WHERE email ILIKE '%j%o%';
| id | first_name | last_name | age | |
|---|---|---|---|---|
| 7 | John | Smith | johnsmith@gmail.com | 25 |
| 6 | Jane | Doe | janedoe@Gmail.com | 28 |
| 5 | Bree | Jensen | bjensen@corp.net | 42 |
| 1 | John | Jacobs | jjacobs@corp.net | 56 |
التعامل مع البيانات المفقودة (NULL)
بعد ذلك، دعنا نستكشف كيفية التعامل مع الصفوف التي تحتوي على بيانات مفقودة في أعمدتها. لتحقيق ذلك، سنضيف عمودًا آخر إلى جدول users الخاص بنا: first_paid_at. سيكون هذا العمود الجديد من نوع TIMESTAMP (مشابه لـ datetime في لغات أخرى)، وسيمثل أول تاريخ ووقت قام فيه المستخدم بالدفع لتطبيقنا. ربما نرغب في إرسال بطاقة تهنئة أو بعض الزهور لهم في ذكرى استخدامهم لتطبيقنا؟ يمكننا إسقاط جدول users باستخدام DROP TABLE users; وإعادة إنشائه، لكن هذا سيؤدي أيضًا إلى حذف جميع البيانات الموجودة في جدولنا. لتعديل جدول دون إسقاطه وفقدان البيانات، يمكننا استخدام ALTER TABLE:
ALTER TABLE users ADD COLUMN first_paid_at TIMESTAMP;
يعيد هذا الأمر النتيجة ALTER TABLE، مما يعني أن استعلام ALTER الخاص بنا قد نجح. إذا قمنا بالاستعلام عن جدول users الآن، سنلاحظ أن هذا العمود الجديد لا يحتوي على أي بيانات:
SELECT * FROM users;
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 1 | John | Smith | johnsmith@gmail.com | 25 | |
| 2 | Jane | Doe | janedoe@Gmail.com | 28 | |
| 3 | Xavier | Wills | xavier@wills.io | 35 | |
| 4 | Bev | Scott | bev@bevscott.com | 16 | |
| 5 | Bree | Jensen | bjensen@corp.net | 42 | |
| 6 | John | Jacobs | jjacobs@corp.net | 56 | |
| 7 | Rick | Fuller | fullman@hotmail.com | 16 |
عمود first_paid_at لدينا فارغ، وتُظهره نتيجة استعلام psql كعمود فارغ. هذا العمود ليس فارغًا تقنيًا؛ بل يحتوي على قيمة خاصة يختار psql عدم عرضها في إخراجه: NULL. تُعد NULL قيمة خاصة في قواعد البيانات، فهي تمثل غياب أو عدم وجود قيمة، ولا تتصرف كما نتوقع. لتوضيح ذلك، دعنا نلقي نظرة على استعلامات SELECT البسيطة التالية:
SELECT 1 = 1, 1 = 2;
| ?column? | ?column? |
|---|---|
| t | f |
هنا، قمنا ببساطة باختيار 1 = 1 و 1 = 2. وكما نتوقع، فإن نتيجة هذين العبارتين هي t و f (أو TRUE و FALSE). الرقم 1 يساوي 1، والرقم 1 لا يساوي 2. الآن، دعنا نجرب نفس الشيء مع NULL:
SELECT 1 = NULL;
| ?column? |
|---|
قد نتوقع أن تكون هذه القيمة FALSE، لكن القيمة المرجعة هي في الواقع NULL. لتصوير قيم NULL بشكل أفضل قليلاً، دعنا نحدد كيفية عرض psql لقيم NULL باستخدام خيار \pset:
fcc=# \pset null 'NULL'
Null display is "NULL".
الآن إذا قمنا بتشغيل هذا الاستعلام مرة أخرى، سنرى إخراج NULL الذي نتوقعه:
SELECT 1 = NULL;
| ?column? |
|---|
| NULL |
إذن، 1 لا يساوي NULL، فماذا عن NULL = NULL؟
SELECT NULL = NULL;
| ?column? |
|---|
| NULL |
الغريب أن NULL لا تساوي NULL. من المفيد التفكير في NULL كقيمة غير معروفة. هل قيمة غير معروفة تساوي 1؟ حسنًا، لا نعرف – إنها غير معروفة. هل قيمة غير معروفة تساوي قيمة غير معروفة أخرى؟ مرة أخرى، إنها غير معروفة. بهذه الطريقة، يصبح مفهوم NULL أكثر منطقية.
استخدام IS NULL و IS NOT NULL
لا يمكننا استخدام معامل المساواة مع NULL، ولكن يمكننا استخدام معاملين مصممين خصيصًا لذلك: IS NULL و IS NOT NULL.
SELECT NULL IS NULL, NULL IS NOT NULL;
| ?column? | ?column? |
|---|---|
| t | f |
تظهر هذه القيم كما هو متوقع: NULL IS NULL صحيحة، و NULL IS NOT NULL خاطئة. هذا كله جيد وغريب، ولكن كيف نستخدم هذا؟ أولاً، دعنا نضع بعض البيانات في عمود first_paid_at الخاص بنا:
UPDATE users SET first_paid_at = NOW() WHERE id = 1;
UPDATE users SET first_paid_at = (NOW() - INTERVAL '1 month') WHERE id = 2;
UPDATE users SET first_paid_at = (NOW() - INTERVAL '1 year') WHERE id = 3;
في عبارات UPDATE أعلاه، قمنا بتعيين عمود first_paid_at لثلاثة مستخدمين مختلفين: المستخدم ذو المعرف 1 إلى الوقت الحالي (NOW())، والمستخدم ذو المعرف 2 إلى تاريخ قبل شهر واحد، والمستخدم ذو المعرف 3 إلى تاريخ قبل عام واحد. أولاً، دعنا نجد المستخدمين الذين دفعوا لنا والمستخدمين الذين لم يدفعوا:
SELECT * FROM users WHERE first_paid_at IS NULL;
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 4 | Bev | Scott | bev@bevscott.com | 16 | NULL |
| 5 | Bree | Jensen | bjensen@corp.net | 42 | NULL |
| 1 | John | Jacobs | jjacobs@corp.net | 56 | NULL |
| 2 | Rick | Fuller | fullman@hotmail.com | 16 | NULL |
SELECT * FROM users WHERE first_paid_at IS NOT NULL;
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 1 | John | Smith | johnsmith@gmail.com | 25 | 2020-08-11 20:49:17.230517 |
| 2 | Jane | Doe | janedoe@Gmail.com | 28 | 2020-07-11 20:49:17.233124 |
| 3 | Xavier | Wills | xavier@wills.io | 35 | 2019-08-11 20:49:17.23488 |
معاملات المقارنة مع التواريخ والأوقات
الآن بعد أن أصبح لدينا بعض البيانات، دعنا نستخدم نفس معاملات المقارنة لدينا مع حقل TIMESTAMP الجديد هذا. لنحاول العثور على المستخدمين الذين دفعوا لنا لأول مرة خلال الأسبوع الماضي. للقيام بذلك، يمكننا أخذ الوقت الحالي، NOW()، وطرح أسبوع واحد منه باستخدام الكلمة المفتاحية INTERVAL:
SELECT * FROM users WHERE first_paid_at > (NOW() - INTERVAL '1 week');
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 1 | John | Smith | johnsmith@gmail.com | 25 | 2020-08-11 20:49:17.230517 |
يمكننا أيضًا استخدام فترة زمنية مختلفة، مثل ثلاثة أشهر مضت:
SELECT * FROM users WHERE first_paid_at < (NOW() - INTERVAL '3 months');
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 3 | Xavier | Wills | xavier@wills.io | 35 | 2019-08-11 20:49:17.23488 |
دعنا نحاول العثور على المستخدمين الذين دفعوا لنا لأول مرة بين شهر واحد وستة أشهر مضت. يمكننا دمج شروطنا مرة أخرى باستخدام AND، ولكن بدلاً من استخدام معاملي “أقل من” و “أكبر من”، دعنا نستخدم الكلمة المفتاحية BETWEEN:
SELECT * FROM users WHERE first_paid_at BETWEEN (NOW() - INTERVAL '6 month') AND (NOW() - INTERVAL '1 month');
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 2 | Jane | Doe | janedoe@Gmail.com | 28 | 2020-07-11 20:49:17.233124 |
التحقق من الوجود باستخدام EXISTS / NOT EXISTS
طريقة أخرى للتحقق من الوجود هي استخدام EXISTS و NOT EXISTS. تقوم هذه المعاملات بتصفية الصفوف عن طريق التحقق من وجود (أو عدم وجود) شرط معين. عادةً ما يكون هذا الشرط عبارة عن استعلام مقابل جدول آخر. لإعداد ذلك، دعنا ننشئ جدولًا جديدًا يسمى posts. سيحتوي هذا الجدول على المنشورات التي يمكن للمستخدم إنشاؤها في نظامنا.
CREATE TABLE posts(
id SERIAL PRIMARY KEY,
body TEXT NOT NULL,
user_id INTEGER REFERENCES users NOT NULL
);
إنه جدول بسيط. يحتوي فقط على معرف (ID)، وحقل لتخزين نص المنشور (body)، ومرجع للمستخدم الذي كتب المنشور (user_id). دعنا نُدرج بعض البيانات في هذا الجدول الجديد:
INSERT INTO posts(body, user_id) VALUES
('Here is post 1', 1),
('Here is post 2', 1),
('Here is post 3', 2),
('Here is post 4', 3);
في البيانات التي أدرجناها في جدول posts، المستخدم ذو المعرف 1 لديه منشورين، والمستخدم ذو المعرف 2 لديه منشور واحد، والمستخدم ذو المعرف 3 لديه أيضًا منشور واحد. للعثور على المستخدمين الذين لديهم منشورات، يمكننا استخدام EXISTS. تأخذ الكلمة المفتاحية EXISTS استعلامًا فرعيًا (subquery). إذا تم إرجاع أي شيء من هذا الاستعلام الفرعي (حتى صف بقيمة NULL فقط)، فستقوم قاعدة البيانات بتضمين هذا الصف في مجموعة النتائج. من وثائق PostgreSQL حول EXISTS: “وسيطة EXISTS هي عبارة SELECT تعسفية، أو استعلام فرعي. يتم تقييم الاستعلام الفرعي لتحديد ما إذا كان يُرجع أي صفوف. إذا أرجع صفًا واحدًا على الأقل، تكون نتيجة EXISTS “صحيحة”؛ وإذا لم يُرجع الاستعلام الفرعي أي صفوف، تكون نتيجة EXISTS “خاطئة”.” تبحث EXISTS فقط عن وجود صف من الاستعلام الفرعي – لا يهم ما يحتويه هذا الصف. إليك مثال للمستخدمين الذين لديهم منشورات باستخدام EXISTS:
SELECT * FROM users WHERE EXISTS (
SELECT 1 FROM posts WHERE posts.user_id = users.id
);
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 1 | John | Smith | johnsmith@gmail.com | 25 | 2020-08-11 20:49:17.230517 |
| 2 | Jane | Doe | janedoe@Gmail.com | 28 | 2020-07-11 20:49:17.233124 |
| 3 | Xavier | Wills | xavier@wills.io | 35 | 2019-08-11 20:49:17.23488 |
كما توقعنا، حصلنا على المستخدمين 1 و 2 و 3. يقوم استعلامنا الفرعي EXISTS بالتحقق من وجود سجل في جدول posts حيث يتطابق user_id الخاص بالمنشور مع عمود id في جدول users. لقد أرجعنا القيمة 1 في جملة SELECT لأنه يمكننا إرجاع أي شيء هنا – فقاعدة البيانات تريد فقط أن ترى أن شيئًا ما قد تم إرجاعه بالفعل. وبالمثل، يمكننا العثور على المستخدمين الذين ليس لديهم أي منشورات عن طريق تغيير EXISTS إلى NOT EXISTS:
SELECT * FROM users WHERE NOT EXISTS (
SELECT 1 FROM posts WHERE posts.user_id = users.id
);
| id | first_name | last_name | age | first_paid_at | |
|---|---|---|---|---|---|
| 4 | Bev | Scott | bev@bevscott.com | 16 | NULL |
| 5 | Bree | Jensen | bjensen@corp.net | 42 | NULL |
| 1 | John | Jacobs | jjacobs@corp.net | 56 | NULL |
| 2 | Rick | Fuller | fullman@hotmail.com | 16 | NULL |
أخيرًا، يمكننا أيضًا إعادة كتابة هذا الاستعلام لاستخدام IN أو NOT IN بدلاً من EXISTS أو NOT EXISTS، على النحو التالي: SELECT * FROM users WHERE users.id IN (SELECT user_id FROM posts);. يعمل هذا من الناحية التقنية، ولكن كقاعدة عامة، إذا كنت تختبر وجود سجل آخر، فمن الأفضل عادةً استخدام EXISTS من حيث الأداء. يُفضل استخدام المعاملين IN و NOT IN للتحقق من قيمة مقابل قائمة ثابتة، كما فعلنا سابقًا: SELECT * FROM users WHERE first_name IN ('John', 'Jane', 'Rick');
معاملات البت (Bitwise Operators)
على الرغم من أن معاملات البت (Bitwise Operators) لا تُستخدم كثيرًا في الممارسة العملية، إلا أننا سنلقي نظرة على مثال بسيط لاستكمال الشرح. إذا أردنا (لسبب ما) عرض أعمار مستخدمينا بالصيغة الثنائية والتلاعب بتلك البتات، يمكننا استخدام مجموعة متنوعة من معاملات البت. كمثال، دعنا نلقي نظرة على معامل “AND” الثنائي: &.
SELECT age::bit(8) & '11111111' FROM users;
| ?column? |
|---|
| 00010000 |
| 00101010 |
| 00111000 |
| 00010000 |
| 00011001 |
| 00011100 |
| 00100011 |
لإجراء عملية حسابية بتية، يجب علينا أولاً تحويل عمود age من عدد صحيح إلى صيغة ثنائية – في هذا المثال، قمنا بتحويله إلى سلسلة ثنائية مكونة من ثمانية بتات باستخدام ::bit(8). بعد ذلك، يمكننا إجراء عملية “AND” الثنائية لنتيجة العمر بالصيغة الثنائية مع سلسلة ثنائية أخرى، 11111111. نظرًا لأن عملية AND الثنائية تُرجع 1 فقط إذا كانت كلتا البتتين 1، فإن هذه السلسلة المكونة من جميع الآحاد تجعل الإخراج مثيرًا للاهتمام. تستخدم جميع معاملات البت الأخرى تقريبًا نفس التنسيق:
SELECT age::bit(8) | '11111111' FROM users; -- bitwise OR
SELECT age::bit(8) # '11111111' FROM users; -- bitwise XOR
SELECT age::bit(8) << '00000001' FROM users; -- bitwise shift left
SELECT age::bit(8) >> '00000001' FROM users; -- bitwise shift right
يختلف معامل “NOT” الثنائي (~) قليلاً في أنه يُطبق على مصطلح واحد فقط – على غرار معامل NOT العادي:
SELECT ~age::bit(8) FROM users;
| ?column? |
|---|
| 11101111 |
| 11010101 |
| 11000111 |
| 11101111 |
| 11100110 |
| 11100011 |
| 11011100 |
وأخيرًا، المعامل الأكثر فائدة بين معاملات البت: الدمج (concatenation). الاستخدام الشائع لهذا المعامل هو دمج سلاسل نصية معًا. على سبيل المثال، إذا أردنا إنشاء خاصية محسوبة لـ “الاسم الكامل” للمستخدمين، يمكننا استخدام الدمج:
SELECT first_name || ' ' || last_name AS name FROM users;
| name |
|---|
| Bev Scott |
| Bree Jensen |
| John Jacobs |
| Rick Fuller |
| John Smith |
| Jane Doe |
| Xavier Wills |
هنا، نقوم بدمج (أو “ربط”) الخاصية first_name، ومسافة (' ')، والخاصية last_name لبناء قيمة name.
الخلاصة التقنية
لقد استعرضنا في هذا الدليل الشامل معظم معاملات تصفية الاستعلامات الأساسية التي ستحتاج إليها في SQL. بينما توجد بعض المعاملات الأخرى التي لم نتناولها، إلا أنها إما نادرة الاستخدام أو تعمل بنفس المبادئ التي شرحناها، ولن تشكل تحديًا كبيرًا. فهم هذه المعاملات هو حجر الزاوية في كتابة استعلامات SQL فعالة ودقيقة، مما يتيح لك استخراج البيانات المطلوبة بمرونة وكفاءة عالية من قواعد البيانات.