يوفّر Android دعمًا مضمّنًا لـ SQLite، وهي قاعدة بيانات SQL فعّالة. اتّبِع أفضل الممارسات التالية لتحسين أداء تطبيقك وضمان بقائه سريعًا مع نمو بياناتك. باتّباع أفضل الممارسات هذه، يمكنك أيضًا تقليل احتمالية مواجهة مشاكل في الأداء يصعب تكرارها وتحديدها وحلّها.
لتحقيق أداء أسرع، اتّبِع مبادئ الأداء التالية:
قراءة صفوف وأعمدة أقل: يمكنك تحسين طلبات البحث لاسترداد البيانات اللازمة فقط. قلِّل كمية البيانات التي تتم قراءتها من قاعدة البيانات، لأنّ استرداد البيانات الزائدة يمكن أن يؤثر في الأداء.
دفع العمل إلى محرّك SQLite: يمكنك إجراء العمليات الحسابية والفلترة والترتيب ضمن طلبات البحث في SQL. يمكن أن يؤدي استخدام محرّك طلبات البحث في SQLite إلى تحسين الأداء بشكل كبير.
تعديل مخطّط قاعدة البيانات: يمكنك تصميم مخطّط قاعدة البيانات لمساعدة SQLite في إنشاء خطط طلبات بحث وتمثيلات بيانات فعّالة. فهرسة الجداول بشكل صحيح وتحسين هياكل الجداول لتحسين الأداء.
بالإضافة إلى ذلك، يمكنك استخدام أدوات تحديد المشاكل وحلّها المتاحة لقياس أداء قاعدة بيانات SQLite للمساعدة في تحديد الجوانب التي تتطلّب التحسين.
ننصحك باستخدام مكتبة Jetpack Room.
ضبط قاعدة البيانات لتحسين الأداء
اتّبِع الخطوات الواردة في هذا القسم لضبط قاعدة البيانات من أجل تحقيق الأداء الأمثل في SQLite.
تفعيل ميزة "التسجيل المسبق"
تنفّذ SQLite عمليات التحويل من خلال إلحاقها بسجلّ، والذي يتم ضغطه أحيانًا في قاعدة البيانات. ويُعرف ذلك باسم تسجيل الكتابة المسبقة (WAL).
فعِّل WAL ما لم تكن تستخدم ATTACH DATABASE.
يمكنك تفعيل WAL من خلال إطار العمل بدلاً من تشغيل PRAGMA journal_mode=WAL
بنفسك. تفعّل Room ميزة WAL تلقائيًا على الأجهزة التي لا تحتوي على ذاكرة وصول عشوائي (RAM) منخفضة.
بدون Room، استخدِم أحد الخيارات التالية:
- طريقة
enableWriteAheadLogging - علامة فتح
ENABLE_WRITE_AHEAD_LOGGING - الطريقة
setWriteAheadLoggingEnabledمنSQLiteOpenHelper
عندما يعرف إطار العمل أنّ قاعدة البيانات تستخدم WAL، يفتح مجموعة من
الاتصالات بحيث يمكن تشغيل عمليات القراءة في الوقت نفسه مع عملية الكتابة، ويطبّق
وضع WAL المتزامن الموضّح في القسم التالي. إذا شغّلت
PRAGMA journal_mode=WAL بنفسك، سيظل إطار العمل يتعامل مع قاعدة البيانات على أنّها
قاعدة بيانات دفتر يومية للتراجع. يستخدم اتصالاً واحدًا، وتحافظ عمليات الإرسال على
الوضع المتزامن التلقائي FULL.
تخفيف وضع المزامنة
يتضمّن SQLite خيارًا للتحكّم في الوضع المتزامن. في وضع FULL، يصدر كل
تثبيت fsync للمساعدة في ضمان وصول البيانات إلى القرص. يؤدي ذلك إلى تحسين متانة البيانات ولكنه يبطئ عمليات الإرسال. باستخدام WAL، لا تتم مزامنة وضع NORMAL في كل عملية تثبيت، ومع ذلك تتم حماية قاعدة البيانات من التلف.
عند تفعيل ميزة WAL من خلال إطار العمل، يطبّق Android الوضع التزامني التلقائي لميزة WAL على الجهاز، وهو NORMAL في مشروع Android المفتوح المصدر (AOSP) لنظام التشغيل Android 9 والإصدارات الأحدث.
يمكن للشركات المصنّعة للأجهزة تغيير هذا الإعداد التلقائي، وقواعد البيانات التي لا تستخدم WAL
تكون تلقائيًا FULL. للتأكد من أن قاعدة البيانات الخاصة بك تستخدم NORMAL، قم بتعيينها عند
فتح قاعدة البيانات:
// When opening the database
val params = SQLiteDatabase.OpenParams.Builder()
.addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
// SYNC_MODE_NORMAL is available in API level 33 and higher.
// On API levels 28 to 32, pass "NORMAL" instead.
.setSynchronousMode(SQLiteDatabase.SYNC_MODE_NORMAL)
.build()
// With SQLiteOpenHelper, call setOpenParams(params) in your constructor.
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL")
في هذا الإعداد، يمكن أن يعود الإجراء commit قبل تخزين البيانات في قرص. في حال إيقاف تشغيل الجهاز، مثلاً عند انقطاع التيار الكهربائي أو حدوث ذعر النواة، قد يتم فقدان البيانات التي تم الالتزام بها. ومع ذلك، بسبب التسجيل، لا تتلف قاعدة البيانات الخاصة بك.
إذا تعطّل تطبيقك فقط، ستظل بياناتك تصل إلى القرص. بالنسبة إلى معظم التطبيقات، يؤدي هذا الإعداد إلى تحسينات في الأداء بدون أي تكلفة مادية.
إدارة عمليات الربط والذاكرة
يحتفظ كل اتصال مفتوح بقاعدة البيانات بذاكرة SQLite: ذاكرة تخزين مؤقت للصفحة، ومُخصِّص lookaside، وذاكرة تخزين مؤقت للعبارات المجمّعة. تظل صفحات ملف قاعدة البيانات أيضًا في ذاكرة التخزين المؤقت لصفحة النواة. اتّبِع الممارسات الواردة في هذا القسم للتحكّم في هذه الذاكرة بدون إبطاء طلبات البحث.
افتح كل قاعدة بيانات مرة واحدة
استخدِم مثيل SQLiteOpenHelper أو RoomDatabase واحدًا لكل ملف قاعدة بيانات
للعملية بأكملها، على سبيل المثال من خلال جعله مثيلاً فريدًا. يحتوي كل مثيل على
مجموعة الاتصال الخاصة به وذاكرة التخزين المؤقت للصفحات وذاكرة التخزين المؤقت للعبارات، ولا يمكن لمثيلين
يفتحان الملف نفسه مشاركتها. في أحد مقاييس الأداء، أدى دمج اثنين من المساعدين اللذين فتحا الملف نفسه في ملف واحد إلى تقليل ذاكرة التخزين المؤقت لصفحة SQLite إلى النصف تقريبًا وتقليل وقت استجابة طلب البحث p95 بأكثر من النصف.
للعثور على نُسخ مكرّرة، ابحث عن dumpsys dbinfo لمسار قاعدة البيانات نفسه ضمن أكثر من عنوان Connection pool for.
إبقاء تغييرات المخطط خارج دالة الاستدعاء المفتوحة
يتم تشغيل دالة رد الاتصال onOpen في SQLiteOpenHelper في كل مرة يتم فيها فتح قاعدة البيانات، وكذلك دالة رد الاتصال onOpen في RoomDatabase.Callback. ضَع عبارات مثل CREATE TABLE IF NOT EXISTS وCREATE INDEX IF NOT EXISTS في onCreate وonUpgrade، أو في عمليات نقل بيانات الغرف. لا تستدعِ
setVersion في onOpen، لأنّ SQLiteOpenHelper يضبط الإصدار
بعد onCreate وonUpgrade. حتى عندما يكون المخطط موجودًا بالفعل، لا تزال هذه العبارات تعمل في كل عملية فتح، ويكتب setVersion في ملف قاعدة البيانات.
تحديد حجم ذاكرة التخزين المؤقت للعبارات المُعدّة مسبقًا
يخزّن Android مؤقتًا العبارات المجمّعة لكل اتصال. تحتفظ ذاكرة التخزين المؤقت بـ 25 عبارة تلقائيًا، ويمكنك رفع هذا العدد إلى MAX_SQL_CACHE_SIZE (100) باستخدام setMaxSqlCacheSize. إذا كانت قاعدة البيانات
تنفّذ أكثر من 25 عبارة مختلفة في مساراتها الساخنة، فسيحدث تضارب في ذاكرة التخزين المؤقت
ويقوم SQLite بتجميع العبارات نفسها مرارًا وتكرارًا.
أولاً، عليك تحديد معلَمات طلبات البحث. لا يمكن لأي حجم ذاكرة تخزين مؤقت أن يساعد في لغة SQL التي تم إنشاؤها باستخدام دمج السلاسل، لأنّ كل سلسلة هي عبارة جديدة. بعد ذلك، اطّلِع على نتائج ذاكرة التخزين المؤقت للعبارات وعدد مرات عدم العثور عليها ضمن POOL STATS في
dumpsys meminfo. على سبيل المثال، قاعدة بيانات phenotype.db في نموذج الناتج في ذلك القسم تحتوي على 147,921 نتيجة ناجحة و89,616 نتيجة غير ناجحة، أي بمعدّل %38 من النتائج غير الناجحة. يمكن أن تؤدي زيادة حجم ذاكرة التخزين المؤقت في قاعدة بيانات مثل هذه إلى إزالة
جميع الأخطاء تقريبًا. تستخدم كل عبارة مخزّنة مؤقتًا بضعة كيلوبايت من الذاكرة.
لزيادة حجم ذاكرة التخزين المؤقت، يمكنك إلغاء onConfigure في الفئة الفرعية SQLiteOpenHelper:
override fun onConfigure(db: SQLiteDatabase) {
db.setMaxSqlCacheSize(SQLiteDatabase.MAX_SQL_CACHE_SIZE)
}
إذا كنت تستخدم Room، يمكنك استدعاء الطريقة نفسها على SupportSQLiteDatabase في دالة الاستدعاء onOpen لـ RoomDatabase.Callback.
إغلاق قواعد البيانات غير النشطة، مع إبقاء القواعد النشطة مفتوحة
تحتفظ قاعدة البيانات المفتوحة باتصالاتها وذاكرة التخزين المؤقت وواصفات الملفات حتى عندما تكون في وضع عدم النشاط. في أحد مقاييس الأداء، احتفظت قاعدة بيانات Room غير نشطة في وضع WAL والتي ظلّت مفتوحة بحوالي 678 كيلوبايت من ذاكرة التخزين المؤقت لصفحة SQLite و5 واصفات ملفات. إذا كنت تستخدم قاعدة بيانات من حين لآخر فقط، مثلاً أثناء المزامنة اليومية، عليك إغلاقها عند الانتهاء.
لإغلاق قواعد البيانات غير النشطة بأمان، اتّبِع الإرشادات التالية:
- يمكن لمكتبة Room إغلاق قاعدة بيانات غير نشطة نيابةً عنك باستخدام
setAutoCloseTimeout، وهي واجهة برمجة تطبيقات تجريبية. تعيد Room فتح قاعدة البيانات عند الوصول التالي وتشغّل دالة الاستدعاءonOpenمرة أخرى، لذا ضع أي إعدادات اتصال هناك. - تجنَّب استخدام الطريقتين
setIdleConnectionTimeoutفيSQLiteOpenHelperوSQLiteDatabase.OpenParams.Builder. تم إيقافها نهائيًا في المستوى 29 من واجهة برمجة التطبيقات لأنّها تغلق الاتصالات بدون استعادة الإعدادات التي طبّقتها باستخدام عباراتPRAGMA. - لا تغلق قاعدة بيانات تستعلم عنها كل بضع ثوانٍ. إذا كانت المهلة أقصر من الفاصل الزمني بين طلبات البحث، سيتم إغلاق قاعدة البيانات وإعادة فتحها بشكل متكرر، ما يؤدي إلى تجاهل ذاكرة التخزين المؤقت للصفحة وذاكرة التخزين المؤقت للعبارات في كل مرة، وبالتالي إبطاء طلب البحث التالي.
مراعاة ذاكرة SQLite
قد يكون من الصعب رصد مشاكل الذاكرة في SQLite. ذاكرة التخزين المؤقت لصفحات SQLite وذاكرة التخزين المؤقت للعبارات هي
أجزاء مخصّصة في الذاكرة لعناصر متعددة. ولكن لا يفعّل Android وحدات الإدخال والإخراج المعيّنة في الذاكرة في SQLite تلقائيًا، لذا يقرأ SQLite ملفات قاعدة البيانات من خلال قراءة الملفات العادية، ومحتوى الملفات الذي تخزّنه النواة مؤقتًا هو صفحات ملفات غير معيّنة. ولا يتم احتسابها ضمن نقاط PSS لتطبيقك في dumpsys meminfo. على الأجهزة التي تستخدم الإصدار الثاني من مجموعة التحكم (cgroup v2) لتدوين سجلات استخدام الذاكرة، لا تزال النواة تحتسب هذه الصفحات ضمن مجموعة التحكم (cgroup) لذاكرة تطبيقك. لتقليل هذه البصمة المخفية، يمكنك قراءة عدد أقل من الصفحات باستخدام الفهارس وقراءة الصفوف والأعمدة التي تحتاجها فقط. يؤدي إغلاق قواعد البيانات التي لا تحتاج إليها
إلى إخلاء مساحة التخزين المؤقت في SQLite.
تحديد مخططات الجداول الفعّالة
لتحسين الأداء وتقليل استهلاك البيانات، حدِّد مخطط جدول فعّال. تنشئ 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-tree، ولكن طلبات البحث التي
تتم فلترتها حسب 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.
By default, SQLite creates a rowid column for your table, where rowid is an
implicit INTEGER PRIMARY KEY AUTOINCREMENT. إذا كان لديك عمود يحمل الاسم INTEGER PRIMARY KEY، سيصبح هذا العمود اسمًا مستعارًا للعمود rowid.
بالنسبة إلى الجداول التي تحتوي على مفتاح أساسي غير INTEGER أو مجموعة من
الأعمدة، ننصحك باستخدام WITHOUT ROWID.
تخزين البيانات الصغيرة كـ BLOB والبيانات الكبيرة كملف
إذا أردت ربط بيانات كبيرة بصف، مثل صورة مصغّرة لصورة أو صورة لجهة اتصال، يمكنك تخزين البيانات إما في عمود BLOB أو في ملف، ثم تخزين المسار في العمود.
يتم تقريب الملفات بشكل عام إلى زيادات تبلغ 4 كيلوبايت. بالنسبة إلى الملفات الصغيرة جدًا، حيث يكون خطأ التقريب كبيرًا، من الأفضل تخزينها في قاعدة البيانات كـ BLOB. يقلّل SQLite من طلبات نظام الملفات، وهو أسرع من نظام الملفات الأساسي في بعض الحالات.
تحسين أداء طلبات البحث
اتبع أفضل الممارسات هذه لتحسين أداء الاستعلام في SQLite عن طريق تقليل أوقات الاستجابة وزيادة كفاءة المعالجة إلى أقصى حد.
قراءة الصفوف التي تحتاجها فقط
تتيح لك الفلاتر تضييق نطاق النتائج من خلال تحديد معايير معيّنة، مثل النطاق الزمني أو الموقع الجغرافي أو الاسم. تتيح لك الحدود التحكم في عدد النتائج التي تراها:
db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Process cursor data
}
}
قراءة الأعمدة التي تحتاجها فقط
تجنب تحديد الأعمدة غير الضرورية، والتي يمكن أن تبطئ استعلاماتك وتهدر الموارد. بدلاً من ذلك، اختَر فقط الأعمدة المستخدَمة.
في المثال التالي، يمكنك اختيار id وname وphone:
// 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)
// Further processing
}
}
ومع ذلك، ما عليك سوى استخدام عمود name:
db.rawQuery("""
SELECT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(0)
// Further processing
}
}
تحديد معلَمات طلبات البحث
قد تتضمّن سلسلة طلب البحث مَعلمة لا تُعرف إلا في وقت التشغيل، مثل ما يلي:
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
}
}
}
في الرمز السابق، ينشئ كل طلب بحث سلسلة مختلفة، وبالتالي لا يستفيد من ذاكرة التخزين المؤقت للعبارات. يتطلب كل استدعاء SQLite لتجميعه
قبل أن يتمكن من تنفيذه. بدلاً من ذلك، يمكنك استبدال الوسيطة id
بمَعلمة
وربط القيمة بـ selectionArgs:
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
}
}
}
الآن يمكن تجميع طلب البحث مرة واحدة وتخزينه مؤقتًا. تتم إعادة استخدام طلب البحث المجمَّع
بين عمليات الاستدعاء المختلفة لـ getNameById(long).
التكرار في SQL وليس في الرمز
استخدِم طلب بحث واحدًا يعرض جميع النتائج المستهدَفة، بدلاً من تكرار برمجي على طلبات بحث SQL لعرض نتائج فردية. تكون الحلقة البرمجية أبطأ بنحو 1000 مرة من طلب بحث SQL واحد.
استخدام DISTINCT للقيم الفريدة
يمكن أن يؤدي استخدام الكلمة الرئيسية DISTINCT إلى تحسين أداء طلبات البحث من خلال
تقليل كمية البيانات التي يجب معالجتها. على سبيل المثال، إذا أردت عرض القيم الفريدة فقط من عمود، استخدِم DISTINCT:
db.rawQuery("""
SELECT DISTINCT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Only iterate over distinct names in Kotlin
// Process distinct name
}
}
استخدام دوال التجميع كلما أمكن ذلك
استخدِم دوال التجميع للحصول على نتائج مجمّعة بدون بيانات الصفوف. على سبيل المثال، يتحقّق الرمز التالي مما إذا كان هناك صف واحد مطابق على الأقل:
// 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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
لجلب الصف الأول فقط، يمكنك استخدام EXISTS() لعرض 0 إذا لم يتوفّر صف مطابق و1 إذا تطابق صف واحد أو أكثر:
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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
استخدِم دوال SQLite التجميعية في رمز تطبيقك:
-
COUNT: تحتسب عدد الصفوف في عمود معيّن. -
SUM: تضيف جميع القيم الرقمية في عمود. -
MINأوMAX: تحدّد أدنى قيمة أو أعلى قيمة. تعمل هذه الدالة مع الأعمدة الرقمية وأنواعDATEوأنواع النصوص. -
AVG: للعثور على متوسط القيمة الرقمية -
GROUP_CONCAT: تدمج السلاسل مع فاصل اختياري.
استخدام COUNT() بدلاً من Cursor.getCount()
في المثال التالي، تقرأ الدالة Cursor.getCount() جميع الصفوف من قاعدة البيانات وتعرض جميع قيم الصفوف:
// 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()
// Use count
}
ومع ذلك، باستخدام COUNT()، تُرجع قاعدة البيانات
العدد فقط:
db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
cursor.moveToFirst()
val count = cursor.getInt(0)
// Use count
}
استخدام طلبات البحث المتداخلة بدلاً من الرموز البرمجية
تتسم لغة SQL بأنّها قابلة للتركيب وتتيح الاستعلامات الفرعية وعمليات الربط وقيود المفاتيح الخارجية. يمكنك استخدام نتيجة طلب بحث واحد في طلب بحث آخر بدون الحاجة إلى استخدام رمز التطبيق. يقلّل ذلك من الحاجة إلى نسخ البيانات من SQLite ويتيح لمحرّك قاعدة البيانات تحسين طلب البحث.
في المثال التالي، يمكنك تشغيل استعلام للعثور على المدينة التي بها أكبر عدد من العملاء، ثم استخدام النتيجة في استعلام آخر للعثور على جميع العملاء من تلك المدينة:
// 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()) {
// Process inner cursor data
}
}
}
}
للحصول على النتيجة في نصف وقت المثال السابق، استخدم استعلام SQL واحدًا مع عبارات متداخلة:
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()) {
// Process cursor data
}
}
التحقّق من التفرّد في SQL
إذا كان يجب عدم إدراج صف ما لم تكن قيمة عمود معينة فريدة في الجدول، فقد يكون من الأكثر فعالية فرض هذا التفرد كقيد عمود.
In the following example, one query is run to validate the row to be inserted and another to actually insert:
// 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
)
)
بدلاً من التحقق من القيد الفريد في Kotlin، يمكنك التحقق منه في 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 بالتحقّق من القيد:
try {
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
arrayOf(customer.id.toString(), customer.name, customer.username)
)
} catch(e: SQLiteConstraintException) {
throw AddCustomerException(customer, e)
}
يتيح SQLite استخدام الفهارس الفريدة مع أعمدة متعددة:
CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);
يتحقّق SQLite من صحة القيود بشكل أسرع وبنفقات عامة أقل من رمز Kotlin. من أفضل الممارسات استخدام SQLite بدلاً من رمز التطبيق.
تجميع عمليات إدراج متعددة في معاملة واحدة
تنفّذ المعاملة عمليات متعددة، ما يحسّن الكفاءة والدقة أيضًا. لتحسين اتساق البيانات وتسريع الأداء، يمكنك تجميع عمليات الإدراج:
db.beginTransaction()
try {
customers.forEach { customer ->
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
arrayOf(customer.id.toString(), customer.name, "customerValue")
)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
خارج المعاملة الصريحة، يتم تشغيل كل عملية كتابة في معاملتها الضمنية الخاصة وتدفع التكلفة الكاملة لعملية الإرسال، بما في ذلك fsync في وضع FULL المتزامن. تؤدي عملية التجميع إلى دفع هذه التكلفة مرة واحدة، ما يجعل مجموعة من عمليات الكتابة أسرع بمقدار أو أكثر.
استخدام أدوات تحديد المشاكل وحلّها
يوفّر 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 Analyzer
توفّر SQLite واجهة سطر الأوامر (CLI) 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
في Android 13 والإصدارات الأحدث، يمكنك أيضًا تسجيل العمليات البطيئة فقط. اضبط حدًا بالملي ثانية، ثم أعِد تشغيل تطبيقك ليقرأ علامة السجلّ:
$ adb shell setprop log.tag.SQLiteSlowQueries VERBOSE
$ adb shell setprop db.log.slow_query_threshold 100
$ adb shell am force-stop <package-name>
تظهر العمليات البطيئة في logcat ضمن علامة SQLiteConnection مع مدتها وSQL ومسار قاعدة البيانات. لضبط الحدّ الأقصى لتطبيق واحد، استخدِم
db.log.slow_query_threshold.<uid>، حيث يمثّل <uid> معرّف UID للتطبيق.
تتبُّع Perfetto
عند ضبط Perfetto، أضِف فئة database atrace لتضمين عمليات SQLite في عملية التتبُّع. لمعرفة ما يفعله كل سلسلة أثناء تشغيل
SQL، أضف أيضًا أحداث المجدول:
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "sched/sched_blocked_reason"
atrace_categories: "database"
}
}
}
يتتبّع Android كل عملية في قاعدة البيانات كشريحة تحمل اسم العملية
ولغة الاستعلامات البنيوية (SQL) الخاصة بها، على سبيل المثال
executeForCursorWindow SELECT * FROM message WHERE body LIKE ?. يحتفظ SQL بعناصر نائبة ? ويتم اقتطاعه إلى 256 حرفًا. ولا يتم تضمين وسيطات الربط. تشمل العمليات open وclose وprepare وexecute وصيغ executeFor. لا يعني ظهور شريحة prepare أنّ البيانات غير متوفرة في ذاكرة التخزين المؤقت للعبارات.
يعتمد تخطيط المسار على إصدار Android:
- في الإصدار 17 من Android والإصدارات الأحدث، يتضمّن كل اتصال مسارًا غير متزامن في عملية التطبيق
يحمل الاسم
SQLiteConnection-متبوعًا برقم الاتصال. تظهر المعاملات على شكل شرائحTRANSACTION-EXCLUSIVEأوTRANSACTION-IMMEDIATEأوTRANSACTION-DEFERRED، مع العبارات التي تم تشغيلها فيها بشكل متداخل تحتها. يحصل أيضًا سلسلة المحادثات التي تشغّل عملية على أحداث فوريةSTART-وEND-على مسار سلسلة المحادثات، لكل معاملة ولكل عبارة خارج المعاملة. تنتهي أسماء هذه الأحداث باسم مسار الاتصال، على سبيل المثال-SQLiteConnection-0. - في الإصدار 16 من نظام التشغيل Android والإصدارات الأقدم، يكون لكل عبارة SQL مميّزة مسار غير متزامن خاص بها في عملية التطبيق، ولا يتم تتبُّع المعاملات.
تبدأ أرقام الاتصال من 0 لكل قاعدة بيانات، لذا فإن الاتصالات الأساسية
لجميع قواعد البيانات في عملية ما تشترك في المسار SQLiteConnection-0. إذا كان تطبيقك يستخدم أكثر من قاعدة بيانات واحدة في الوقت نفسه، يمكنك قياس المدد باستخدام الحدثين START- وEND- في مسارات سلاسل المحادثات.
الشكل 1: فحص كامل للجدول في سلسلة التعليمات الرئيسية في Android 17
يمكنك تحليل هذه الأجزاء باستخدام PerfettoSQL في واجهة مستخدم Perfetto أو باستخدام
trace_processor. يعثر طلب البحث التالي على عمليات SQLite الأكثر تكرارًا في كل عملية. يعمل طلب البحث هذا على جميع إصدارات Android:
INCLUDE PERFETTO MODULE slices.with_context;
SELECT process_name, name AS operation, COUNT(*) AS count
FROM process_slice
WHERE name GLOB 'open *' OR name GLOB 'close *'
OR name GLOB 'prepare *' OR name GLOB 'execute*'
OR name GLOB 'TRANSACTION-*'
GROUP BY process_name, name
ORDER BY count DESC
LIMIT 10;
يعرض طلب البحث التالي عمليات SQLite التي استغرقت أكثر من ملي ثانية واحدة على سلسلة التعليمات الرئيسية في Android 17 والإصدارات الأحدث:
INCLUDE PERFETTO MODULE slices.with_context;
WITH marks AS (
SELECT process_name, utid, is_main_thread, ts, name,
LEAD(ts) OVER (PARTITION BY utid ORDER BY ts) AS end_ts,
LEAD(name) OVER (PARTITION BY utid ORDER BY ts) AS end_name
FROM thread_slice
WHERE name GLOB 'START-*' OR name GLOB 'END-*'
)
SELECT process_name, ts, ROUND((end_ts - ts) / 1e6, 1) AS dur_ms,
substr(name, 7) AS operation
FROM marks
WHERE name GLOB 'START-*'
AND end_name = 'END-' || substr(name, 7)
AND is_main_thread
AND end_ts - ts > 1e6
ORDER BY dur_ms DESC
LIMIT 10;
الشكل 2: عمليات SQLite التي تستغرق أكثر من ملي ثانية واحدة على سلسلة التعليمات الرئيسية
يعثر طلب البحث التالي على سلاسل المحادثات التي تكتب خارج معاملة صريحة، حيث تدفع كل عملية كتابة مقابل عملية الإرسال الخاصة بها، في Android 17 والإصدارات الأحدث:
INCLUDE PERFETTO MODULE slices.with_context;
WITH ops AS (
SELECT process_name, thread_name, utid, name,
upper(ltrim(substr(name, instr(name, ' ') + 1))) AS sql
FROM thread_slice
WHERE name GLOB 'START-*'
)
SELECT process_name, thread_name,
SUM(name GLOB 'START-TRANSACTION-*') AS transactions,
SUM(name GLOB 'START-execute*' AND (sql GLOB 'INSERT *'
OR sql GLOB 'UPDATE *' OR sql GLOB 'DELETE *' OR sql GLOB 'REPLACE *'))
AS autocommit_writes
FROM ops
GROUP BY utid
HAVING autocommit_writes > 0
ORDER BY autocommit_writes DESC
LIMIT 10;
dumpsys meminfo
يطبع الأمر adb shell dumpsys meminfo <package-name> الإحصاءات ذات الصلة باستخدام التطبيق للذاكرة، بما في ذلك تفاصيل حول SQLite في قسم DATABASES. على سبيل المثال، تم أخذ هذا من مخرجات 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
يطبع نظام التشغيل Android 14 والإصدارات الأحدث الجدولَين PER CONNECTION STATS وPOOL STATS
الموضَّحَين في المثال. يطبع نظام التشغيل Android 13 والإصدارات الأقدم جدولاً لكل اتصال فقط،
مع دمج إحصاءات ذاكرة التخزين المؤقت في عمود واحد cache على النحو التالي:
hits/misses/size.
ضمن PER CONNECTION STATS، ستجد ما يلي:
-
pgsz: حجم صفحة واحدة في قاعدة البيانات بالكيلوبايت -
dbsz: حجم ملف قاعدة البيانات بالكامل، بالكيلوبايت -
Lookaside(b): عدد فتحات الذاكرة المؤقتة التي يستخدمها الاتصال Despite the(b)in the header, this is a count of slots, not bytes. -
cache hitsوcache missesوcache size: إحصاءات حول ذاكرة التخزين المؤقت للعبارات المُعدّة مسبقًا في الاتصال تحتسب هذه القيم عبارات SQL المجمّعة، وليس صفحات قاعدة البيانات. -
Dbname: المسار إلى ملف قاعدة البيانات يتم إلحاق رقم بين قوسين بالاتصالات الإضافية بقاعدة البيانات نفسها، مثل(1). باستخدام WAL، يمكن أن تحتوي قاعدة البيانات على عدة اتصالات بحيث يمكن تشغيل عمليات القراءة بالتوازي.
ضمن POOL STATS، ستجد إحصاءات ذاكرة التخزين المؤقت للعبارات المُعدّة لكل
مجموعة اتصالات:
cache hits: يخزّن SQLite العبارات المُعدّة مؤقتًا ويحاول إعادة استخدامها عند تشغيل طلبات البحث، لتوفير بعض الجهد والذاكرة في تجميع عبارات SQL. يشير ذلك إلى عدد مرات العثور على العبارات في ذاكرة التخزين المؤقت.-
cache misses: عدد البيانات غير المتوفرة في ذاكرة التخزين المؤقت للعبارات -
cache size: في الإصدار Android 16 QPR2 والإصدارات الأحدث، يشير هذا إلى عدد العبارات المُعدّة في ذاكرة التخزين المؤقت، والتي يتم جمعها من خلال الاتصالات غير النشطة في المجموعة. في الإصدارات من Android 14 إلى Android 16 QPR1، تمثّل هذه القيمة مجموع النتائج الناجحة وغير الناجحة، ولا تمثّل حجم ذاكرة التخزين المؤقت.
استخدِم هذه الإحصاءات لتحديد المشاكل التي يجب حلّها:
- ويعني ارتفاع معدّل عدم العثور على البيانات أنّ SQLite يجمّع العبارات نفسها بشكل متكرّر.
أولاً، ابحث عن SQL الذي تم إنشاؤه باستخدام دمج السلاسل وحدِّد مَعلماته. إذا كانت طلبات البحث قد تم تحديد مَعلماتها بالفعل، وكان الحد الأقصى لعدد
cache sizeلكل اتصال هو 25، ارفع حجم ذاكرة التخزين المؤقت. - إذا ظهر مسار قاعدة البيانات أكثر من مرة بدون لاحقة على نمط
(1)، يعني ذلك أنّ تطبيقك فتح الملف نفسه مرتين. افتح كل قاعدة بيانات مرة واحدة. - تظل قاعدة البيانات التي نادرًا ما يستخدمها تطبيقك تحتفظ بالذاكرة أثناء فتحها. أغلِقها عند الانتهاء.
dumpsys dbinfo
adb shell dumpsys dbinfo <package-name> يطبع حالة كل مجموعة اتصالات في تطبيقك. يمكنك إضافة -v لإدراج العبارات أيضًا في ذاكرة التخزين المؤقت للعبارات المُعدّة لكل اتصال. في ما يلي مثال على الناتج من Android 17، مع اختصار المسارات والأسطر الطويلة:
** Database info for pid 2705 [com.example.app] **
Connection pool for /data/user/0/com.example.app/databases/settings.db:
Open: true
Max connections: 4
Total execution time (ms): 4963
Total statements executed: 231239
Average time per statement (ms): 0
Configuration: openFlags=805306368, ..., journalMode=WAL, syncMode=NORMAL
IsReadOnlyDatabase: false
Available primary connection:
<none>
Available non-primary connections:
<none>
Acquired connections:
<none>
Connection waiters:
<none>
Connection pool for /data/user/0/com.example.app/databases/events.db:
Open: true
Max connections: 1
Total execution time (ms): 842
Total statements executed: 840
Average time per statement (ms): 1
Configuration: openFlags=268435456, ..., journalMode=TRUNCATE, syncMode=FULL
IsReadOnlyDatabase: false
Available primary connection:
Connection #0:
isPrimaryConnection: true
onlyAllowReadOnlyOperations: false
totalLongOperations: 0
Most recently executed operations:
0: [10-26 04:22:32.133] executeForChangedRowCount took 0ms -
succeeded, sql="DELETE FROM params WHERE app_id=?", path=...
1: [10-26 04:22:32.133] prepare took 0ms - succeeded,
sql="DELETE FROM params WHERE app_id=?", path=...
...
ابحث عن ما يلي:
-
Configuration: وضع دفتر اليومية والوضع المتزامن للمجمّع في الإصدار 13 من Android والإصدارات الأحدث، يعرض هذا السطر الأوضاع التي طبّقها إطار العمل، بما في ذلك الإعدادات التلقائية للنظام الأساسي. في المثال، يستخدمevents.dbسجلّ التراجع مع وضع المزامنةFULL، لذا ينتظر كل تثبيت إجراء مزامنة كاملة. لا يعكس هذا السطر عباراتPRAGMAالتي تنفّذها بنفسك. Max connections: تشير القيمة 1 لقاعدة بيانات قابلة للكتابة إلى أنّ إطار العمل لا يعتبر قاعدة البيانات في وضع WAL، وبالتالي تشترك عمليات القراءة والكتابة في اتصال واحد.Total execution timeوTotal statements executedوAverage time per statement: تتوفّر في الإصدار 13 من نظام التشغيل Android والإصدارات الأحدث. ويمكنك استخدامها للعثور على قواعد البيانات الأكثر ازدحامًا.-
Most recently executed operations: آخر 20 عملية على كل اتصال، مع مدتها وSQL. ابحث عن أنماط مثل تكرار طلب البحث نفسه في حلقة، أو عمليات كتابة متعددة على التوالي خارج المعاملة. في الإصدار QPR1 من Android 15 والإصدارات الأحدث، تتضمّن هذه القائمة أيضًا المعاملات الصريحة. -
totalLongOperationsوOperations exceeding 2000ms: يتوفّران في الإصدار Android 15 QPR1 والإصدارات الأحدث. وتعرض عدد العمليات والمعاملات التي استغرقت أكثر من ثانيتين، وتسرد آخر 10 منها. -
Acquired connectionsوConnection waiters: الاتصالات قيد الاستخدام وسلاسل المحادثات التي تنتظر الاتصال تشير عمليات الانتظار إلى حدوث تعارض، مثل معاملة كتابة طويلة تحظر عمليات الكتابة الأخرى. -
Prepared statement cache: تمت طباعتها باستخدام-v. تشير العديد من الإدخالات التي تختلف فقط في القيم الحرفية إلى SQL الذي تم إنشاؤه باستخدام دمج السلسلة. - يشير المسار نفسه ضمن أكثر من عنوان
Connection pool forإلى أنّ تطبيقك فتح الملف نفسه مرتين.
مراجع إضافية
محتوى المشاهدات
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- تشغيل مقاييس الأداء في عملية "الدمج المتواصل"
- اللقطات الثابتة
- إنشاء "ملفات الأداء الأساسي" وقياسها بدون استخدام Macrobenchmark