المفاهيم وتنفيذ Jetpack Compose
يوفّر Android دعمًا مضمّنًا لـ SQLite، وهي قاعدة بيانات SQL فعّالة. اتّبِع أفضل الممارسات التالية لتحسين أداء تطبيقك وضمان بقائه سريعًا مع نمو بياناتك. باتّباع أفضل الممارسات هذه، يمكنك أيضًا تقليل احتمالية مواجهة مشاكل في الأداء يصعب تكرارها وتحديدها وحلّها.
لتحقيق أداء أسرع، اتّبِع مبادئ الأداء التالية:
قراءة صفوف وأعمدة أقل: يمكنك تحسين طلبات البحث لاسترداد البيانات اللازمة فقط. قلِّل كمية البيانات التي تتم قراءتها من قاعدة البيانات، لأنّ استرداد البيانات الزائدة يمكن أن يؤثر في الأداء.
دفع العمل إلى محرّك SQLite: يمكنك إجراء العمليات الحسابية والفلترة والترتيب ضمن طلبات البحث في SQL. يمكن أن يؤدي استخدام محرّك طلبات البحث في SQLite إلى تحسين الأداء بشكل كبير.
تعديل مخطّط قاعدة البيانات: يمكنك تصميم مخطّط قاعدة البيانات لمساعدة SQLite في إنشاء خطط طلبات بحث وتمثيلات بيانات فعّالة. فهرسة الجداول بشكل صحيح وتحسين هياكل الجداول لتحسين الأداء.
بالإضافة إلى ذلك، يمكنك استخدام أدوات تحديد المشاكل وحلّها المتاحة لقياس أداء قاعدة بيانات SQLite للمساعدة في تحديد الجوانب التي تتطلّب التحسين.
ننصحك باستخدام مكتبة Jetpack Room.
ضبط قاعدة البيانات لتحسين الأداء
اتّبِع الخطوات الواردة في هذا القسم لضبط قاعدة البيانات من أجل تحقيق الأداء الأمثل في SQLite.
تخفيف وضع المزامنة
يتضمّن SQLite خيارًا للتحكّم في الوضع المتزامن. في وضع FULL، يصدر كل
تثبيت fsync للمساعدة في ضمان وصول البيانات إلى القرص. يؤدي ذلك إلى تحسين متانة البيانات ولكنه يبطئ عمليات الإرسال. باستخدام WAL، لا تتم مزامنة وضع NORMAL في كل عملية تثبيت، ومع ذلك تتم حماية قاعدة البيانات من التلف.
عند تفعيل ميزة WAL من خلال إطار العمل، يطبّق Android الوضع التزامني التلقائي لميزة WAL على الجهاز، وهو NORMAL في مشروع Android المفتوح المصدر (AOSP) لنظام التشغيل Android 9 والإصدارات الأحدث.
يمكن للشركات المصنّعة للأجهزة تغيير هذا الإعداد التلقائي، وقواعد البيانات التي لا تستخدم WAL
تكون تلقائيًا FULL. للتأكد من أن قاعدة البيانات الخاصة بك تستخدم NORMAL، قم بتعيينها عند
فتح قاعدة البيانات:
Kotlin
// 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")
Java
// When opening the database
SQLiteDatabase.OpenParams params = new 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 عن طريق تقليل أوقات الاستجابة وزيادة كفاءة المعالجة إلى أقصى حد.
قراءة الصفوف التي تحتاجها فقط
تتيح لك الفلاتر تضييق نطاق النتائج من خلال تحديد معايير معيّنة، مثل النطاق الزمني أو الموقع الجغرافي أو الاسم. تتيح لك الحدود التحكم في عدد النتائج التي تراها:
Kotlin
db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Process cursor data
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""", null)) {
while (cursor.moveToNext()) {
// Process cursor data
}
}
قراءة الأعمدة التي تحتاجها فقط
تجنب تحديد الأعمدة غير الضرورية، والتي يمكن أن تبطئ استعلاماتك وتهدر الموارد. بدلاً من ذلك، اختَر فقط الأعمدة المستخدَمة.
في المثال التالي، يمكنك اختيار 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)
// Further processing
}
}
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);
// Further processing
}
}
ومع ذلك، ما عليك سوى استخدام عمود name:
Kotlin
db.rawQuery("""
SELECT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(0)
// Further processing
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT name
FROM Customers;
""", null)) {
while (cursor.moveToNext()) {
String name = cursor.getString(0);
// Further processing
}
}
تحديد معلَمات طلبات البحث
قد تتضمّن سلسلة طلب البحث مَعلمة لا تُعرف إلا في وقت التشغيل، مثل ما يلي:
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).
استخدام 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
// Process distinct name
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT DISTINCT name
FROM Customers;
""", null)) {
while (cursor.moveToNext()) {
// Only iterate over distinct names in Java
// Process distinct name
}
}
استخدام دوال التجميع كلما أمكن ذلك
استخدِم دوال التجميع للحصول على نتائج مجمّعة بدون بيانات الصفوف. على سبيل المثال، يتحقّق الرمز التالي مما إذا كان هناك صف واحد مطابق على الأقل:
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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
لجلب الصف الأول فقط، يمكنك استخدام 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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
استخدِم دوال 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()
// Use count
}
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();
// Use count
}
ومع ذلك، باستخدام COUNT()، تُرجع قاعدة البيانات
العدد فقط:
Kotlin
db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
cursor.moveToFirst()
val count = cursor.getInt(0)
// Use count
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""", null)) {
cursor.moveToFirst();
int count = cursor.getInt(0);
// Use count
}
استخدام طلبات البحث المتداخلة بدلاً من الرموز البرمجية
تتسم لغة 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()) {
// Process inner cursor data
}
}
}
}
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()) {
// Process inner cursor data
}
}
}
}
للحصول على النتيجة في نصف وقت المثال السابق، استخدم استعلام 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()) {
// Process cursor data
}
}
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()) {
// Process cursor data
}
}
التحقّق من التفرّد في SQL
إذا كان يجب عدم إدراج صف ما لم تكن قيمة عمود معينة فريدة في الجدول، فقد يكون من الأكثر فعالية فرض هذا التفرد كقيد عمود.
In the following example, one query is run to validate the row to be inserted and another to actually insert:
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, "customerValue")
)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
Java
db.beginTransaction();
try {
for (Customer customer : customers) {
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
new String[] {
String.valueOf(customer.id),
customer.name,
"customerValue"
});
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- تشغيل مقاييس الأداء في عملية "الدمج المتواصل"
- اللقطات الثابتة
- إنشاء "ملفات الأداء الأساسي" وقياسها بدون استخدام Macrobenchmark