Menu
flag Ar iconالعربيةdown icon

دعم JSON في SQLite: json_extract و json_set و json_each

كيف تخزّن SQLite بيانات JSON وتستعلم عنها: استخراج الحقول، تحديث القيم، توسيع المصفوفات بـ json_each، وفهرسة مسارات JSON لرفع الأداء.

تحتوي هذه الصفحة على محررات قابلة للتشغيل — حرّر، شغّل، وشاهد النتيجة فوراً.

SQLite بلا نوع JSON خاص — ولا مشكلة في ذلك

لا يوفّر SQLite نوع عمود مخصصًا لتخزين JSON. ببساطة، يُخزَّن الـ JSON داخل عمود TEXT عادي، وتتكفّل مجموعة من الدوال المدمجة — يُطلق عليها مجتمعةً اسم امتداد JSON1 — بمهمة تحليله والاستعلام عنه وتعديله. هذا الامتداد متوفّر تلقائيًا في كل إصدار حديث من SQLite، فلا حاجة لتثبيت أي شيء.

الفكرة باختصار: خزِّن المستند كنص، واستعن بالدوال للغوص داخله.

سطران فقط، كل واحد فيهما يحمل مستند JSON داخل عمود نصي عادي. الآن نحتاج طرقًا للوصول إلى ما بداخل هذه المستندات.

استخراج الحقول باستخدام json_extract والمعامل ->>

تقوم الدالة json_extract(column, path) بسحب قيمة معيّنة من مستند JSON. يبدأ المسار بالرمز $ (الذي يمثّل الجذر)، ويُستخدم .field للوصول إلى مفاتيح الكائنات، و[i] للوصول إلى عناصر المصفوفات حسب الفهرس.

كتابة json_extract(data, '$.name') في كل مكان يصير ممل بسرعة، فلهذا توفّر لك SQLite مُعاملَين مختصرَين:

  • -> يُرجع القيمة بصيغة JSON (السلاسل النصية ترجع مع علامات التنصيص).
  • ->> يُرجع قيمة SQL عادية (نص أو رقم، بدون علامات تنصيص).

name_json يرجع كقيمة "Ada" (لا تزال بصيغة JSON)، أما name_text فيرجع كـ Ada مباشرة. استخدم ->> حين تريد قيمة جاهزة للمقارنة أو العرض، واستخدم -> حين تنوي تمرير النتيجة إلى دالة JSON أخرى.

فلترة السجلات بناءً على حقول JSON

بعد أن أتقنت استخراج القيم، تستطيع الفلترة بسهولة. ضع التعبير في جملة WHERE كأي شرط عادي:

هذا يعمل، لكن مع جدول كبير نسبياً ستلاحظ بطئاً واضحاً، لأن كل صف يحتاج إلى تحليل (parse) كامل لتقييم الشرط. سنعالج هذه المشكلة بعد قليل عبر إنشاء فهرس مناسب.

بناء JSON باستخدام json_object و json_array

في الاتجاه المعاكس، يمكنك تركيب قيم JSON مباشرة داخل الاستعلام:

json_object('k1', v1, 'k2', v2, ...) تُنشئ كائن JSON، بينما json_array(v1, v2, ...) تُنشئ مصفوفة. هاتان الدالتان مفيدتان جدًا لتجميع استجابات الـ API مباشرةً داخل SQL، كما أنهما تدعمان التداخل بكل سهولة:

تحديث الـ JSON: json_set و json_insert و json_replace

في sqlite توجد ثلاث دوال متقاربة تُعدّل مستند JSON وتُرجع نسخة جديدة منه:

  • json_set(doc, path, value) — يضبط القيمة عند المسار المحدّد، فيُنشئه إن لم يكن موجوداً ويستبدله إن كان موجوداً.
  • json_insert(doc, path, value) — يُضيف القيمة فقط إذا لم يكن المسار موجوداً مسبقاً.
  • json_replace(doc, path, value) — يُحدّث القيمة فقط إذا كان المسار موجوداً فعلاً.

انتبه إلى أن هذه الدوال لا تُعدّل المستند في مكانه، بل تُرجع مستنداً جديداً تكتبه عادةً مرة أخرى عبر جملة UPDATE:

لاحظ أن json_set يقبل أكثر من زوج (مسار/قيمة) في الاستدعاء الواحد. ولحذف مفتاح معيّن، استخدم json_remove(doc, path).

توسيع المصفوفات باستخدام json_each

الدالة json_each هي دالة تُرجع جدولاً (table-valued function): تأخذ مصفوفة JSON (أو كائناً) وتُعيد صفاً مستقلاً لكل عنصر. بهذه الطريقة يتحوّل سؤال مثل "أعطني المستخدمين الذين يحملون وسم admin" — وهو سؤال مزعج في SQL العادي — إلى عملية ربط (JOIN) عادية تماماً:

يتم ربط كل صف من جدول users بعناصر مصفوفة tags الخاصة به. وتُتيح لك json_each أعمدة مفيدة مثل key وvalue وtype وfullkey. أما شقيقتها json_tree فهي تجوب المستند بأكمله بشكل تعاودي وتشمل كل عقدة متداخلة فيه — وهذا مفيد جدًا حين تبحث في مستندات لا تعرف شكلها مسبقًا.

فهرسة حقل JSON في SQLite

الاستعلام WHERE data ->> '$.active' = 1 الذي رأيناه قبل قليل يعمل بلا مشاكل، لكن SQLite مضطر لتحليل كل صف حتى يُقيّم الشرط. ولو كنت تستعلم عن حقل معيّن بكثرة، فالأفضل أن تُنشئ فهرسًا تعبيريًا (expression index) عليه:

يجب أن يستخدم الفهرس نفس التعبير الموجود في استعلامك بالضبط. لو خلطت بين json_extract(data, '$.email') في الفهرس و data ->> '$.email' في الاستعلام، فلن يتطابقا، وسيبقى الفهرس مهملاً دون استخدام — فاختر صيغة واحدة والتزم بها.

أما الحقول التي تستعلم عنها باستمرار، فالأفضل استخدام عمود محسوب (generated column) لأنه أوضح وأسهل في القراءة:

email يظهر كأنه عمود عادي بالنسبة لمن يكتب الاستعلامات، لكنه يبقى متزامنًا مع الـ JSON تلقائيًا.

التحقق من صحة JSON

تُعيد الدالة json_valid(text) القيمة 1 إذا كان النص قابلًا للتحليل كـ JSON، و0 في غير ذلك. استخدمها مع قيد CHECK لرفض البيانات غير الصالحة عند الكتابة:

الإدراج الأول ينجح، أما الثاني فيفشل بسبب خطأ في القيد. لو لم يكن هذا الفحص موجودًا، لبقي JSON المشوّه ساكنًا في الجدول بهدوء، إلى أن ينفجر استدعاء json_extract ما بعد أشهر من الآن.

الفرق بين jsonb و json في SQLite

منذ الإصدار 3.45 من SQLite، ظهر تمثيل ثنائي اسمه JSONB — نفس البيانات، لكن مُحلَّلة مسبقًا إلى صيغة ثنائية مضغوطة، حتى لا تعيد الدوال تحليلها مع كل استدعاء. عائلة الدوال jsonb_* (مثل jsonb_extract وjsonb_set وjsonb_object، وغيرها) تُرجع JSONB بدل النص، كما يمكن الاستعلام عن أعمدة JSONB بنفس المعاملات المعتادة.

استخدم JSON النصي العادي عندما تريد أن تكون المستندات مقروءة بشكل واضح في عمليات التفريغ (dumps) وسهلة الفحص. أما JSONB فاستعن به حين يكون الجدول ضخمًا، كثير الاستعلام، ويظهر فعلًا حِمل التحليل (parsing overhead) في أدوات قياس الأداء. لا تبدّل الافتراضي بلا داعٍ — وضوح JSON النصي يستحق الكثير أثناء تصحيح الأخطاء.

متى يكون تخزين JSON في SQLite هو الخيار الصحيح؟

أعمدة JSON تتألق في الحالات التالية:

  • عندما يختلف شكل البيانات من سطر لآخر (مثل حمولات الأحداث، وسجلات التدقيق، وويب هوكس التكاملات).
  • عندما تخزّن استجابة من واجهة API خارجية وتريد الاحتفاظ بها كما هي.
  • عندما يكون الحقل نادر الاستعلام ولا يُستخدم في التصفية تقريبًا.

وتكون خيارًا سيئًا في الحالات التالية:

  • عندما تستخدم JSON تهرّبًا من تصميم مخطط (schema) مناسب. إذا كانت جميع الصفوف تحمل الحقول نفسها، فهذه أعمدة وليست JSON.
  • عندما تحتاج إلى التصفية أو الربط (join) على قيمة معيّنة بشكل متكرر. عمود حقيقي مع فهرس سيتفوّق دائمًا على البحث عبر مسار JSON.
  • عندما تكون بحاجة إلى مفاتيح خارجية (foreign keys). JSON لا يوفّر تكاملًا علائقيًا.

النقطة المثالية هي المزج بين الاثنين: أعمدة قياسية للحقول التي تقود الاستعلامات والقيود، وعمود JSON بجوارها لاستيعاب الذيل الطويل من البيانات المتغيّرة.

التالي: البحث النصي الكامل

JSON يمنحك المرونة على صعيد التخزين. الصفحة التالية تتناول FTS5 — محرّك البحث النصي الكامل في SQLite — الذي يوفّر بحثًا نصيًا حقيقيًا مع ترتيب النتائج وإبراز الكلمات، وهو يتجاوز بأشواط ما يستطيع LIKE فعله.

الأسئلة الشائعة

كيف تُخزَّن بيانات JSON داخل SQLite؟

لا يوجد في SQLite نوع مخصّص لـ JSON، فالبيانات تُحفَظ كنصّ عادي من نوع TEXT. لكن امتداد JSON1 المدمج افتراضيًا منذ الإصدار 3.38 يوفّر دوالّ مثل json_extract و json_set و json_each تتعامل مع هذا النص وتحلّله. ومنذ الإصدار 3.45 صار هناك أيضًا تنسيق ثنائي اسمه JSONB يُسرّع الوصول المتكرر للبيانات.

كيف أستعلم عن عمود JSON في SQLite؟

استخدم json_extract(column, '$.path') أو الاختصار ->>. مثلًا الاستعلام SELECT data ->> '$.name' FROM users يجلب قيمة الحقل name من مستند JSON المخزَّن في العمود data. تبدأ المسارات بـ $ للجذر، ثم .field لمفاتيح الكائنات، و [i] لفهارس المصفوفات.

هل يمكن فهرسة حقل JSON في SQLite؟

نعم، عبر إنشاء فهرس على تعبير المسار المستخرج، مثل: CREATE INDEX idx_user_email ON users(json_extract(data, '$.email')). أي استعلام يستعمل نفس التعبير في جملة WHERE سيستفيد من هذا الفهرس. وللحقول التي تُستعلَم باستمرار، يكون استخدام عمود محسوب (generated column) مع فهرس عليه حلًّا أنظف غالبًا.

ما الفرق بين -> و ->> في SQLite؟

المعامل -> يُرجع قيمة JSON (تظل بصيغة JSON، أي أن النصوص تعود بين علامتي اقتباس)، بينما ->> يُرجع قيمة SQL عادية (نصّ أو رقم بدون اقتباس). استخدم ->> حين تريد القيمة الخام للعرض أو للمقارنة، واستخدم -> حين تنوي متابعة عمليات JSON أخرى عليها.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن