يتيح نظام التشغيل Android إمكانية استخدام SQLite المضمّنة، وهي قاعدة بيانات SQL فعّالة. اتّبِع أفضل الممارسات التالية لتحسين أداء تطبيقك، ما يضمن بقاءه سريعًا وسريعًا بشكل متوقّع مع زيادة حجم بياناتك. ومن خلال اتّباع أفضل الممارسات هذه، يمكنك أيضًا تقليل احتمالية مواجهة مشاكل في الأداء يصعب إعادة إنتاجها وتحديد المشاكل المتعلّقة بها وحلّها.
لتحقيق أداء أسرع، اتّبِع مبادئ الأداء التالية:
قراءة عدد أقل من الصفوف والأعمدة: حسِّن طلبات البحث لاسترداد البيانات الضرورية فقط. قلِّل مقدار البيانات التي تتم قراءتها من قاعدة البيانات، لأنّ استرداد البيانات الزائدة يمكن أن يؤثّر في الأداء.
نقل العمل إلى محرّك SQLite: تنفيذ العمليات الحسابية والتصفية والفرز ضمن استعلامات SQL يمكن أن يؤدي استخدام محرّك طلبات البحث في SQLite إلى تحسين الأداء بشكل كبير.
تعديل مخطط قاعدة البيانات: صمِّم مخطط قاعدة البيانات لمساعدة SQLite في إنشاء خطط استعلام وتمثيلات بيانات فعّالة. فهرسة الجداول بشكل سليم وتحسين بنية الجداول لتحسين الأداء
بالإضافة إلى ذلك، يمكنك استخدام أدوات تحديد المشاكل وحلّها المتاحة لقياس أداء قاعدة بيانات SQLite للمساعدة في تحديد المجالات التي تتطلّب تحسينًا.
ننصح باستخدام مكتبة Jetpack Room.
ضبط قاعدة البيانات لتحسين الأداء
اتّبِع الخطوات الواردة في هذا القسم لضبط قاعدة البيانات لتحقيق أفضل أداء في SQLite.
تفعيل ميزة "كتابة السجلّات قبل التنفيذ"
تنفِّذ SQLite عمليات التغيير عن طريق إلحاقها بسجلّ، ثم يتم ضغط هذا السجلّ بشكل دوري في قاعدة البيانات. يُعرف ذلك باسم تسجيل الكتابة المسبقة.
فعِّل
WAL
إلا إذا كنت تستخدم ATTACH
DATABASE
.
تخفيف وضع المزامنة
عند استخدام WAL، يتم تلقائيًا إصدار fsync
مع كل عملية تنفيذ للمساعدة في ضمان وصول البيانات إلى القرص. يؤدي ذلك إلى تحسين متانة البيانات، ولكنّه يبطئ عمليات التنفيذ.
يتضمّن SQLite خيارًا للتحكّم في وضع المزامنة. في حال تفعيل WAL، اضبط الوضع المتزامن على NORMAL
:
Kotlin
db.execSQL("PRAGMA synchronous = NORMAL")
Java
db.execSQL("PRAGMA synchronous = NORMAL");
في هذا الإعداد، يمكن أن يتم إرجاع عملية الإيداع قبل تخزين البيانات في القرص. في حال إيقاف تشغيل الجهاز، مثلاً عند انقطاع الطاقة أو حدوث خطأ في النواة، قد يتم فقدان البيانات التي تم حفظها. ومع ذلك، وبسبب التسجيل، لن تتلف قاعدة البيانات.
إذا تعطل تطبيقك فقط، ستصل بياناتك إلى القرص. بالنسبة إلى معظم التطبيقات، يؤدي هذا الإعداد إلى تحسينات في الأداء بدون تكلفة كبيرة.
تحديد مخططات الجداول الفعّالة
لتحسين الأداء وتقليل استهلاك البيانات، حدِّد مخطط جدول فعّالاً. تنشئ SQLite خطط استعلام وبيانات فعّالة، ما يؤدي إلى استرداد البيانات بشكل أسرع. يقدّم هذا القسم أفضل الممارسات لإنشاء مخططات الجداول.
جرِّب INTEGER PRIMARY KEY
.
في هذا المثال، حدِّد جدولاً واملأه على النحو التالي:
CREATE TABLE Customers(
id INTEGER,
name TEXT,
city TEXT
);
INSERT INTO Customers Values(456, 'John Lennon', 'Liverpool, England');
INSERT INTO Customers Values(123, 'Michael Jackson', 'Gary, IN');
INSERT INTO Customers Values(789, 'Dolly Parton', 'Sevier County, TN');
يكون ناتج الجدول على النحو التالي:
rowid | id | اسم | مدينة |
---|---|---|---|
1 | 456 | جون لينون | ليفربول، إنجلترا |
2 | 123 | مايكل جاكسون | غاري، إنديانا |
3 | 789 | دولي بارتون | مقاطعة سيفير، تينيسي |
العمود rowid
هو فهرس يحافظ على ترتيب الإدراج. يتم تنفيذ طلبات البحث التي يتم فلترتها حسب rowid
كبحث سريع في شجرة B، ولكن طلبات البحث التي يتم فلترتها حسب id
هي عملية بحث بطيئة في الجدول.
إذا كنت تخطّط لإجراء عمليات بحث حسب id
، يمكنك تجنُّب تخزين العمود rowid
لتوفير مساحة التخزين وتسريع قاعدة البيانات بشكل عام:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
city TEXT
);
يظهر جدولك الآن على النحو التالي:
id | اسم | مدينة |
---|---|---|
123 | مايكل جاكسون | غاري، إنديانا |
456 | جون لينون | ليفربول، إنجلترا |
789 | دولي بارتون | مقاطعة سيفير، تينيسي |
بما أنّك لست بحاجة إلى تخزين العمود rowid
، تكون طلبات البحث id
سريعة. يُرجى العِلم
أنّه تم الآن ترتيب الجدول استنادًا إلى id
بدلاً من ترتيب الإدراج.
تسريع طلبات البحث باستخدام الفهارس
تستخدم SQLite
الفهارس
لتسريع الاستعلامات. عند فلترة (WHERE
) عمود أو ترتيبه (ORDER BY
) أو تجميع بياناته (GROUP BY
)، إذا كان الجدول يتضمّن فهرسًا للعمود، يتم تسريع طلب البحث.
في المثال السابق، تتطلّب الفلترة حسب city
فحص الجدول بأكمله:
SELECT id, name
WHERE city = 'London, England';
بالنسبة إلى تطبيق يتضمّن الكثير من طلبات البحث عن المدن، يمكنك تسريع هذه الطلبات باستخدام فهرس:
CREATE INDEX city_index ON Customers(city);
يتم تنفيذ الفهرس كجدول إضافي، ويتم ترتيبه حسب عمود الفهرس
ويتم ربطه بـ rowid
:
مدينة | rowid |
---|---|
غاري، إنديانا | 2 |
ليفربول، إنجلترا | 1 |
مقاطعة سيفير، تينيسي | 3 |
يُرجى العِلم أنّ تكلفة التخزين للعمود city
أصبحت مضاعفة الآن، لأنّه أصبح متوفّرًا في كل من الجدول الأصلي والفهرس. بما أنّك تستخدم الفهرس، فإنّ تكلفة مساحة التخزين الإضافية تستحقّ الاستفادة من سرعة تنفيذ طلبات البحث.
ومع ذلك، لا تحتفظ بفهرس لا تستخدمه لتجنُّب دفع تكلفة التخزين بدون تحقيق أي تحسّن في أداء طلبات البحث.
إنشاء فهارس متعدّدة الأعمدة
إذا كانت طلبات البحث تجمع بين عدة أعمدة، يمكنك إنشاء فهارس متعددة الأعمدة لتسريع طلب البحث بالكامل. يمكنك أيضًا استخدام فهرس على عمود خارجي والسماح بإجراء البحث الداخلي كبحث خطي.
على سبيل المثال، إذا كان لديك طلب البحث التالي:
SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name
يمكنك تسريع طلب البحث باستخدام فهرس متعدّد الأعمدة بالترتيب نفسه المحدّد في طلب البحث:
CREATE INDEX city_name_index ON Customers(city, name);
ومع ذلك، إذا كان لديك فهرس فقط على city
، سيتم تسريع الترتيب الخارجي، بينما يتطلب الترتيب الداخلي إجراء بحث خطي.
يمكنك أيضًا استخدام هذه الطريقة مع الاستعلامات التي تتضمّن بادئة. على سبيل المثال، يؤدي الفهرس
ON Customers (city, name)
أيضًا إلى تسريع الفلترة والترتيب والتجميع
حسب city
، لأنّ جدول الفهرس لفهرس متعدّد الأعمدة يتم ترتيبه حسب
الفهارس المحدّدة بالترتيب المحدّد.
جرِّب WITHOUT ROWID
.
تنشئ SQLite تلقائيًا عمودًا rowid
للجدول، حيث يكون rowid
عبارة عن INTEGER PRIMARY KEY AUTOINCREMENT
ضمني. إذا كان لديك عمود INTEGER PRIMARY KEY
، سيصبح هذا العمود اسمًا مستعارًا للعمود rowid
.
بالنسبة إلى الجداول التي تحتوي على مفتاح أساسي غير INTEGER
أو مجموعة من الأعمدة، يمكنك استخدام WITHOUT
ROWID
.
تخزين البيانات الصغيرة كـ BLOB
والبيانات الكبيرة كملف
إذا أردت ربط بيانات كبيرة بصف، مثل صورة مصغّرة أو صورة لجهة اتصال، يمكنك تخزين البيانات إما في عمود BLOB
أو في ملف، ثم تخزين مسار الملف في العمود.
يتم عادةً تقريب أحجام الملفات إلى أقرب مضاعف للرقم 4 كيلوبايت. بالنسبة إلى الملفات الصغيرة جدًا، حيث يكون خطأ التقريب كبيرًا، يكون من الأفضل تخزينها في قاعدة البيانات كـ BLOB
. تقلّل SQLite من طلبات نظام الملفات، وهي أسرع من نظام الملفات الأساسي في بعض الحالات.
تحسين أداء طلبات البحث
اتّبِع أفضل الممارسات التالية لتحسين أداء طلبات البحث في SQLite من خلال تقليل أوقات الاستجابة وزيادة كفاءة المعالجة إلى أقصى حدّ.
قراءة الصفوف التي تحتاج إليها فقط
تتيح لك الفلاتر تضييق نطاق النتائج من خلال تحديد معايير معيّنة، مثل النطاق الزمني أو الموقع الجغرافي أو الاسم. تتيح لك الحدود التحكّم في عدد النتائج التي تظهر لك:
Kotlin
db.rawQuery(""" SELECT name FROM Customers LIMIT 10; """.trimIndent(), null ).use { cursor -> while (cursor.moveToNext()) { ... } }
Java
try (Cursor cursor = db.rawQuery(""" SELECT name FROM Customers LIMIT 10; """, null)) { while (cursor.moveToNext()) { ... } }
قراءة الأعمدة التي تحتاج إليها فقط
تجنَّب اختيار أعمدة غير ضرورية، لأنّ ذلك قد يؤدي إلى إبطاء طلبات البحث وإهدار الموارد. بدلاً من ذلك، اختَر الأعمدة المستخدَمة فقط.
في المثال التالي، يتم اختيار id
وname
وphone
:
Kotlin
// This is not the most efficient way of doing this. // See the following example for a better approach. db.rawQuery( """ SELECT id, name, phone FROM customers; """.trimIndent(), null ).use { cursor -> while (cursor.moveToNext()) { val name = cursor.getString(1) // ... } }
Java
// This is not the most efficient way of doing this. // See the following example for a better approach. try (Cursor cursor = db.rawQuery(""" SELECT id, name, phone FROM customers; """, null)) { while (cursor.moveToNext()) { String name = cursor.getString(1); ... } }
ومع ذلك، تحتاج فقط إلى عمود name
:
Kotlin
db.rawQuery(""" SELECT name FROM Customers; """.trimIndent(), null ).use { cursor -> while (cursor.moveToNext()) { val name = cursor.getString(0) ... } }
Java
try (Cursor cursor = db.rawQuery(""" SELECT name FROM Customers; """, null)) { while (cursor.moveToNext()) { String name = cursor.getString(0); ... } }
تحديد معلَمات لطلبات البحث
قد تتضمّن سلسلة طلب البحث مَعلمة لا تكون معروفة إلا في وقت التشغيل، مثل ما يلي:
Kotlin
fun getNameById(id: Long): String? db.rawQuery( "SELECT name FROM customers WHERE id=$id", null ).use { cursor -> return if (cursor.moveToFirst()) { cursor.getString(0) } else { null } } }
Java
@Nullable public String getNameById(long id) { try (Cursor cursor = db.rawQuery( "SELECT name FROM customers WHERE id=" + id, null)) { if (cursor.moveToFirst()) { return cursor.getString(0); } else { return null; } } }
في الرمز البرمجي السابق، ينشئ كل طلب بحث سلسلة مختلفة، وبالتالي لا يستفيد من ذاكرة التخزين المؤقت للعبارات. يتطلّب كل طلب من SQLite تجميع
هذا الطلب قبل تنفيذه. بدلاً من ذلك، يمكنك استبدال الوسيطة id
بمَعلمة وربط القيمة بـ selectionArgs
:
Kotlin
fun getNameById(id: Long): String? { db.rawQuery( """ SELECT name FROM customers WHERE id=? """.trimIndent(), arrayOf(id.toString()) ).use { cursor -> return if (cursor.moveToFirst()) { cursor.getString(0) } else { null } } }
Java
@Nullable public String getNameById(long id) { try (Cursor cursor = db.rawQuery(""" SELECT name FROM customers WHERE id=? """, new String[] {String.valueOf(id)})) { if (cursor.moveToFirst()) { return cursor.getString(0); } else { return null; } } }
يمكن الآن تجميع طلب البحث مرة واحدة وتخزينه مؤقتًا. تتم إعادة استخدام طلب البحث المجمّع بين عمليات استدعاء مختلفة للدالة getNameById(long)
.
التكرار في SQL وليس في الرمز
استخدِم طلب بحث واحدًا يعرض جميع النتائج المستهدَفة، بدلاً من حلقة برمجية تتكرّر على طلبات بحث SQL لعرض نتائج فردية. تكون الحلقة البرمجية أبطأ بحوالي 1000 مرة من استعلام SQL واحد.
استخدام DISTINCT
للقيم الفريدة
يمكن أن يؤدي استخدام الكلمة الرئيسية DISTINCT
إلى تحسين أداء طلبات البحث من خلال تقليل كمية البيانات التي يجب معالجتها. على سبيل المثال، إذا أردت عرض القيم الفريدة فقط من عمود، استخدِم DISTINCT
:
Kotlin
db.rawQuery(""" SELECT DISTINCT name FROM Customers; """.trimIndent(), null ).use { cursor -> while (cursor.moveToNext()) { // Only iterate over distinct names in Kotlin ... } }
Java
try (Cursor cursor = db.rawQuery(""" SELECT DISTINCT name FROM Customers; """, null)) { while (cursor.moveToNext()) { // Only iterate over distinct names in Java ... } }
استخدام الدوال التجميعية كلما أمكن ذلك
استخدِم الدوال التجميعية للحصول على نتائج مجمّعة بدون بيانات الصفوف. على سبيل المثال، يتحقّق الرمز التالي ممّا إذا كان هناك صف واحد على الأقل مطابق:
Kotlin
// This is not the most efficient way of doing this. // See the following example for a better approach. db.rawQuery(""" SELECT id, name FROM Customers WHERE city = 'Paris'; """.trimIndent(), null ).use { cursor -> if (cursor.moveToFirst()) { // At least one customer from Paris ... } else { // No customers from Paris ... }
Java
// This is not the most efficient way of doing this. // See the following example for a better approach. try (Cursor cursor = db.rawQuery(""" SELECT id, name FROM Customers WHERE city = 'Paris'; """, null)) { if (cursor.moveToFirst()) { // At least one customer from Paris ... } else { // No customers from Paris ... } }
لجلب الصف الأول فقط، يمكنك استخدام EXISTS()
لعرض 0
إذا لم يكن هناك صف مطابق، و1
إذا كان هناك صف واحد أو أكثر مطابق:
Kotlin
db.rawQuery(""" SELECT EXISTS ( SELECT null FROM Customers WHERE city = 'Paris'; ); """.trimIndent(), null ).use { cursor -> if (cursor.moveToFirst() && cursor.getInt(0) == 1) { // At least one customer from Paris ... } else { // No customers from Paris ... } }
Java
try (Cursor cursor = db.rawQuery(""" SELECT EXISTS ( SELECT null FROM Customers WHERE city = 'Paris' ); """, null)) { if (cursor.moveToFirst() && cursor.getInt(0) == 1) { // At least one customer from Paris ... } else { // No customers from Paris ... } }
استخدِم دوال التجميع في SQLite في رمز تطبيقك:
-
COUNT
: تحسب عدد الصفوف في عمود. SUM
: تضيف جميع القيم الرقمية في عمود.-
MIN
أوMAX
: تحدّد هذه السمة أدنى قيمة أو أعلى قيمة. تعمل مع الأعمدة الرقمية وأنواعDATE
وأنواع النصوص. AVG
: تعرض متوسط القيمة الرقمية.GROUP_CONCAT
: لربط السلاسل باستخدام فاصل اختياري.
استخدام COUNT()
بدلاً من Cursor.getCount()
في المثال التالي، تقرأ الدالة Cursor.getCount()
جميع الصفوف من قاعدة البيانات وتعرض جميع قيم الصفوف:
Kotlin
// This is not the most efficient way of doing this. // See the following example for a better approach. db.rawQuery(""" SELECT id FROM Customers; """.trimIndent(), null ).use { cursor -> val count = cursor.getCount() }
Java
// This is not the most efficient way of doing this. // See the following example for a better approach. try (Cursor cursor = db.rawQuery(""" SELECT id FROM Customers; """, null)) { int count = cursor.getCount(); ... }
ومع ذلك، باستخدام COUNT()
، تعرض قاعدة البيانات العدد فقط:
Kotlin
db.rawQuery(""" SELECT COUNT(*) FROM Customers; """.trimIndent(), null ).use { cursor -> cursor.moveToFirst() val count = cursor.getInt(0) }
Java
try (Cursor cursor = db.rawQuery(""" SELECT COUNT(*) FROM Customers; """, null)) { cursor.moveToFirst(); int count = cursor.getInt(0); ... }
تضمين طلبات بحث Nest بدلاً من الرمز
يمكن إنشاء SQL من عدة أجزاء، كما تتيح الاستعلامات الفرعية وعمليات الربط وقيود المفتاح الخارجي. يمكنك استخدام نتيجة طلب بحث في طلب بحث آخر بدون الحاجة إلى كتابة رمز برمجي للتطبيق. يقلّل ذلك من الحاجة إلى نسخ البيانات من SQLite ويسمح لمحرك قاعدة البيانات بتحسين طلب البحث.
في المثال التالي، يمكنك تنفيذ طلب بحث للعثور على المدينة التي تضم أكبر عدد من العملاء، ثم استخدام النتيجة في طلب بحث آخر للعثور على جميع العملاء من تلك المدينة:
Kotlin
// This is not the most efficient way of doing this. // See the following example for a better approach. db.rawQuery(""" SELECT city FROM Customers GROUP BY city ORDER BY COUNT(*) DESC LIMIT 1; """.trimIndent(), null ).use { cursor -> if (cursor.moveToFirst()) { val topCity = cursor.getString(0) db.rawQuery(""" SELECT name, city FROM Customers WHERE city = ?; """.trimIndent(), arrayOf(topCity)).use { innerCursor -> while (innerCursor.moveToNext()) { ... } } } }
Java
// This is not the most efficient way of doing this. // See the following example for a better approach. try (Cursor cursor = db.rawQuery(""" SELECT city FROM Customers GROUP BY city ORDER BY COUNT(*) DESC LIMIT 1; """, null)) { if (cursor.moveToFirst()) { String topCity = cursor.getString(0); try (Cursor innerCursor = db.rawQuery(""" SELECT name, city FROM Customers WHERE city = ?; """, new String[] {topCity})) { while (innerCursor.moveToNext()) { ... } } } }
للحصول على النتيجة في نصف الوقت الذي استغرقه المثال السابق، استخدِم استعلام SQL واحدًا مع عبارات متداخلة:
Kotlin
db.rawQuery(""" SELECT name, city FROM Customers WHERE city IN ( SELECT city FROM Customers GROUP BY city ORDER BY COUNT (*) DESC LIMIT 1; ); """.trimIndent(), null ).use { cursor -> if (cursor.moveToNext()) { ... } }
Java
try (Cursor cursor = db.rawQuery(""" SELECT name, city FROM Customers WHERE city IN ( SELECT city FROM Customers GROUP BY city ORDER BY COUNT(*) DESC LIMIT 1 ); """, null)) { while(cursor.moveToNext()) { ... } }
التحقّق من التفرد في SQL
إذا كان يجب عدم إدراج صف إلا إذا كانت قيمة عمود معيّن فريدة في الجدول، قد يكون من الأفضل فرض هذه التفردية كقيد على العمود.
في المثال التالي، يتم تنفيذ استعلام واحد للتحقّق من صحة الصف الذي سيتم إدراجه، واستعلام آخر لإدراجه فعليًا:
Kotlin
// This is not the most efficient way of doing this. // See the following example for a better approach. db.rawQuery( """ SELECT EXISTS ( SELECT null FROM customers WHERE username = ? ); """.trimIndent(), arrayOf(customer.username) ).use { cursor -> if (cursor.moveToFirst() && cursor.getInt(0) == 1) { throw AddCustomerException(customer) } } db.execSQL( "INSERT INTO customers VALUES (?, ?, ?)", arrayOf( customer.id.toString(), customer.name, customer.username ) )
Java
// This is not the most efficient way of doing this. // See the following example for a better approach. try (Cursor cursor = db.rawQuery(""" SELECT EXISTS ( SELECT null FROM customers WHERE username = ? ); """, new String[] { customer.username })) { if (cursor.moveToFirst() && cursor.getInt(0) == 1) { throw new AddCustomerException(customer); } } db.execSQL( "INSERT INTO customers VALUES (?, ?, ?)", new String[] { String.valueOf(customer.id), customer.name, customer.username, });
بدلاً من التحقّق من القيد الفريد في Kotlin أو Java، يمكنك التحقّق منه في SQL عند تحديد الجدول:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
username TEXT UNIQUE
);
تؤدي SQLite الوظيفة نفسها كما يلي:
CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);
يمكنك الآن إدراج صف والسماح لـ SQLite بالتحقّق من القيد:
Kotlin
try { db.execSql( "INSERT INTO Customers VALUES (?, ?, ?)", arrayOf(customer.id.toString(), customer.name, customer.username) ) } catch(e: SQLiteConstraintException) { throw AddCustomerException(customer, e) }
Java
try { db.execSQL( "INSERT INTO Customers VALUES (?, ?, ?)", new String[] { String.valueOf(customer.id), customer.name, customer.username, }); } catch (SQLiteConstraintException e) { throw new AddCustomerException(customer, e); }
تتيح SQLite فهارس فريدة تتضمّن أعمدة متعدّدة:
CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);
تتحقّق SQLite من صحة القيود بشكل أسرع وبنفقات أقل من رمز Kotlin أو Java. من أفضل الممارسات استخدام SQLite بدلاً من رمز التطبيق.
إجراء عمليات إدراج متعددة في دفعة واحدة ضمن معاملة واحدة
تنفّذ المعاملة عمليات متعدّدة، ما يحسّن ليس فقط الكفاءة، بل أيضًا الدقة. لتحسين اتساق البيانات وتسريع الأداء، يمكنك تجميع عمليات الإدراج على النحو التالي:
Kotlin
db.beginTransaction() try { customers.forEach { customer -> db.execSql( "INSERT INTO Customers VALUES (?, ?, ...)", arrayOf(customer.id.toString(), customer.name, ...) ) } } finally { db.endTransaction() }
Java
db.beginTransaction(); try { for (customer : Customers) { db.execSQL( "INSERT INTO Customers VALUES (?, ?, ...)", new String[] { String.valueOf(customer.id), customer.name, ... }); } } finally { db.endTransaction() }
استخدام أدوات تحديد المشاكل وحلّها
توفّر SQLite أدوات تحديد المشاكل وحلّها التالية للمساعدة في قياس الأداء.
استخدام طلب SQLite التفاعلي
يمكنك تشغيل SQLite على جهازك لتنفيذ طلبات البحث والتعرّف على المزيد من المعلومات.
تستخدم إصدارات منصة Android المختلفة مراجعات مختلفة من SQLite. لاستخدام المحرّك نفسه المتوفّر على جهاز يعمل بنظام التشغيل Android، استخدِم adb shell
وشغِّل sqlite3
على جهازك المستهدف.
يمكنك أن تطلب من SQLite تحديد وقت الاستعلامات:
sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...
EXPLAIN QUERY PLAN
يمكنك أن تطلب من SQLite شرح الطريقة التي تنوي بها الإجابة عن استعلام باستخدام
EXPLAIN QUERY PLAN
:
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers
يتطلّب المثال السابق إجراء بحث كامل في الجدول بدون فهرس للعثور على جميع العملاء من باريس. ويُطلق على ذلك اسم التعقيد الخطي. يجب أن يقرأ SQLite جميع الصفوف وأن يحتفظ فقط بالصفوف التي تتطابق مع العملاء من باريس. لحلّ هذه المشكلة، يمكنك إضافة فهرس:
sqlite> CREATE INDEX Idx1 ON Customers(city);
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SEARCH test USING INDEX Idx1 (city=?
إذا كنت تستخدم واجهة الأوامر التفاعلية، يمكنك أن تطلب من SQLite أن تشرح دائمًا خطط الاستعلامات:
sqlite> .eqp on
لمزيد من المعلومات، يُرجى الاطّلاع على تخطيط طلب البحث.
أداة تحليل SQLite
توفّر SQLite واجهة سطر الأوامر sqlite3_analyzer
لعرض معلومات إضافية يمكن استخدامها لتحديد المشاكل وحلّها. للتثبيت، انتقِل إلى
صفحة تنزيل SQLite.
يمكنك استخدام adb pull
لتنزيل ملف قاعدة بيانات من جهاز مستهدف إلى محطة العمل الخاصة بك من أجل تحليله:
adb pull /data/data/<app_package_name>/databases/<db_name>.db
SQLite Browser
يمكنك أيضًا تثبيت أداة واجهة المستخدم الرسومية SQLite Browser على صفحة التنزيلات في SQLite.
تسجيل البيانات في Android
يسجّل نظام التشغيل Android مدة تنفيذ طلبات بحث SQLite ويسجّلها لك:
# Enable query time logging
$ adb shell setprop log.tag.SQLiteTime VERBOSE
# Disable query time logging
$ adb shell setprop log.tag.SQLiteTime ERROR
Perfetto tracing
عند ضبط إعدادات Perfetto، يمكنك إضافة ما يلي لتضمين عمليات تتبُّع لطلبات بحث فردية:
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
atrace_categories: "database"
}
}
}
dumpsys meminfo
ستعرض adb shell dumpsys meminfo <package-name>
إحصاءات متعلقة باستخدام التطبيق للذاكرة، بما في ذلك بعض التفاصيل حول ذاكرة SQLite. على سبيل المثال، تم أخذ هذا النص من ناتج adb shell dumpsys meminfo com.google.android.gms.persistent
على جهاز أحد المطوّرين:
DATABASES
pgsz dbsz Lookaside(b) cache hits cache misses cache size Dbname
PER CONNECTION STATS
4 52 45 8 41 6 /data/user/10/com.google.android.gms/databases/gaia-discovery
4 8 0 0 0 (attached) temp
4 52 56 5 23 6 /data/user/10/com.google.android.gms/databases/gaia-discovery (1)
4 252 95 233 124 12 /data/user_de/10/com.google.android.gms/databases/phenotype.db
4 8 0 0 0 (attached) temp
4 252 17 0 17 1 /data/user_de/10/com.google.android.gms/databases/phenotype.db (1)
4 9280 105 103169 69805 25 /data/user/10/com.google.android.gms/databases/phenotype.db
4 20 0 0 0 (attached) temp
4 9280 108 13877 6394 25 /data/user/10/com.google.android.gms/databases/phenotype.db (2)
4 8 0 0 0 (attached) temp
4 9280 105 12548 5519 25 /data/user/10/com.google.android.gms/databases/phenotype.db (3)
4 8 0 0 0 (attached) temp
4 9280 107 18328 7886 25 /data/user/10/com.google.android.gms/databases/phenotype.db (1)
4 8 0 0 0 (attached) temp
4 36 51 156 29 5 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
4 36 97 47 27 10 /data/user/10/com.google.android.gms/databases/context_feature_default.db
4 36 56 3 16 4 /data/user/10/com.google.android.gms/databases/context_feature_default.db (2)
4 300 40 2111 24 5 /data/user/10/com.google.android.gms/databases/gservices.db
4 300 39 3 17 4 /data/user/10/com.google.android.gms/databases/gservices.db (1)
4 20 17 0 14 1 /data/user/10/com.google.android.gms/databases/gms.notifications.db
4 20 33 1 15 2 /data/user/10/com.google.android.gms/databases/gms.notifications.db (1)
4 120 40 143 163 4 /data/user/10/com.google.android.gms/databases/android_pay
4 120 123 86 32 19 /data/user/10/com.google.android.gms/databases/android_pay (1)
4 28 33 4 17 3 /data/user/10/com.google.android.gms/databases/googlesettings.db
POOL STATS
cache hits cache misses cache size Dbname
13 68 81 /data/user/10/com.google.android.gms/databases/gaia-discovery
233 145 378 /data/user_de/10/com.google.android.gms/databases/phenotype.db
147921 89616 237537 /data/user/10/com.google.android.gms/databases/phenotype.db
156 30 186 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
50 57 107 /data/user/10/com.google.android.gms/databases/context_feature_default.db
2114 43 2157 /data/user/10/com.google.android.gms/databases/gservices.db
1 31 32 /data/user/10/com.google.android.gms/databases/gms.notifications.db
229 197 426 /data/user/10/com.google.android.gms/databases/android_pay
4 18 22 /data/user/10/com.google.android.gms/databases/googlesettings.db
ضمن DATABASES
، ستجد ما يلي:
pgsz
: حجم صفحة واحدة من قاعدة البيانات بالكيلوبايت-
dbsz
: حجم قاعدة البيانات بأكملها، بالصفحات للحصول على الحجم بالكيلوبايت، اضربpgsz
فيdbsz
. -
Lookaside(b)
: الذاكرة المخصّصة لمخزن lookaside المؤقت في SQLite لكل اتصال، بالبايت. عادةً ما تكون هذه الأخطاء صغيرة جدًا. cache hits
: تحتفظ SQLite بذاكرة تخزين مؤقت لصفحات قاعدة البيانات. هذا هو عدد نتائج ذاكرة التخزين المؤقت للصفحات.-
cache misses
: عدد عمليات عدم العثور على البيانات في ذاكرة التخزين المؤقت للصفحة (العدد). cache size
: عدد الصفحات في ذاكرة التخزين المؤقت (العدد). للحصول على الحجم بالكيلوبايت، اضرب هذا الرقم فيpgsz
.Dbname
: مسار ملف قاعدة البيانات في مثالنا، تحتوي بعض قواعد البيانات على(1)
أو رقم آخر مُلحق باسمها، للإشارة إلى أنّه يوجد أكثر من اتصال واحد بقاعدة البيانات الأساسية نفسها. يتم تتبُّع الإحصاءات لكل عملية ربط.
ضمن POOL STATS
، ستجد ما يلي:
cache hits
: تخزّن SQLite عبارات SQL المُعدّة مسبقًا في ذاكرة التخزين المؤقت وتحاول إعادة استخدامها عند تنفيذ طلبات البحث، وذلك لتوفير بعض الجهد والذاكرة في تجميع عبارات SQL. هذا هو عدد النتائج المطابقة في ذاكرة التخزين المؤقت للبيانات (العدد).-
cache misses
: عدد الأخطاء في ذاكرة التخزين المؤقت للبيانات (عدد). -
cache size
: بدءًا من الإصدار 17 من نظام التشغيل Android، تعرض هذه السمة إجمالي عدد العبارات المُعدّة في ذاكرة التخزين المؤقت. في الإصدارات السابقة، كانت هذه القيمة تساوي مجموع النتائج الإيجابية والسلبية المدرَجة في العمودَين الآخرَين، ولا تمثّل حجم ذاكرة التخزين المؤقت.
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- تشغيل مقاييس الأداء في عملية الدمج المستمر
- اللقطات الثابتة
- إنشاء ملفات Baseline Profiles وقياسها بدون Macrobenchmark