يتيح Android دعمًا مضمّنًا لـ SQLite، وهي قاعدة بيانات SQL فعّالة. اتّبِع أفضل الممارسات التالية لتحسين أداء تطبيقك، ما يضمن بقاءه سريعًا وبسرعة متوقّعة مع زيادة بياناتك. من خلال اتّباع أفضل الممارسات هذه، يمكنك أيضًا تقليل احتمالية مواجهة مشاكل في الأداء يصعب إعادة إنتاجها وتحديد المشاكل وحلّها.
لتحقيق أداء أسرع، اتّبِع مبادئ الأداء التالية:
قراءة عدد أقل من الصفوف والأعمدة: حسِّن طلبات البحث لاسترداد البيانات الضرورية فقط. قلِّل كمية البيانات التي تتم قراءتها من قاعدة البيانات، لأنّ استرداد البيانات الزائدة يمكن أن يؤثر في الأداء.
نقل العمل إلى محرّك SQLite: نفِّذ عمليات الحساب والفلترة والفرز ضمن طلبات بحث SQL. يمكن أن يؤدي استخدام محرّك طلبات البحث في SQLite إلى تحسين الأداء بشكلٍ كبير.
تعديل مخطط قاعدة البيانات: صمِّم مخطط قاعدة البيانات لمساعدة SQLite في إنشاء خطط طلبات بحث وتمثيلات بيانات فعّالة. يمكنك فهرسة الجداول بشكلٍ صحيح وتحسين هياكل الجداول لتحسين الأداء.
بالإضافة إلى ذلك، يمكنك استخدام أدوات تحديد المشاكل وحلّها المتاحة لقياس أداء قاعدة بيانات SQLite للمساعدة في تحديد المجالات التي تتطلب التحسين.
ننصحك باستخدام مكتبة Jetpack Room.
ضبط قاعدة البيانات لتحسين الأداء
اتّبِع الخطوات الواردة في هذا القسم لضبط قاعدة البيانات لتحقيق الأداء الأمثل في SQLite.
تخفيف وضع المزامنة
عند استخدام WAL، يؤدي كل تأكيد تلقائيًا إلى إصدار fsync للمساعدة في ضمان وصول البيانات إلى القرص. يؤدي ذلك إلى تحسين متانة البيانات، ولكنّه يبطئ عمليات التأكيد.
يتضمّن SQLite خيارًا للتحكّم في الوضع المتزامن. إذا فعّلت WAL، اضبط الوضع المتزامن على NORMAL:
Kotlin
// When opening the database
val paramsBuilder: SQLiteDatabase.OpenParams.Builder = SQLiteDatabase.OpenParams.Builder()
paramsBuilder.journalMode = SQLiteDatabase.SYNC_MODE_NORMAL
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");
Java
// When opening the database
SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder();
paramsBuilder.setJournalMode(SQLiteDatabase.SYNC_MODE_NORMAL);
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");
في هذا الإعداد، يمكن أن يتم عرض نتيجة التأكيد قبل تخزين البيانات على القرص. في حال إيقاف الجهاز، مثلاً بسبب انقطاع التيار الكهربائي أو حدوث ذعر النواة، قد يتم فقدان البيانات التي تم تأكيدها. ومع ذلك، لا تتلف قاعدة البيانات بسبب التسجيل.
إذا تعرّض تطبيقك فقط للتعطّل، ستصل بياناتك إلى القرص. بالنسبة إلى معظم التطبيقات، يؤدي هذا الإعداد إلى تحسينات في الأداء بدون تكلفة مادية.
تحسين أداء طلبات البحث
اتّبِع أفضل الممارسات التالية لتحسين أداء طلبات البحث في 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
إذا كان يجب عدم إدراج صف إلا إذا كانت قيمة عمود معيّن فريدة في الجدول، قد يكون من الأكثر فعالية فرض هذا التفرد كقيد عمود.
في المثال التالي، يتم تشغيل طلب بحث واحد للتحقق من صحة الصف الذي سيتم إدراجه وطلب بحث آخر لإدراجه فعليًا:
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")
)
}
} finally {
db.endTransaction()
}
Java
db.beginTransaction();
try {
for (customer : Customers) {
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
new String[] {
String.valueOf(customer.id),
customer.name,
"customerValue"
});
}
} finally {
db.endTransaction()
}
اقتراحات مخصصة لك
- ملاحظة: يتم عرض نص الرابط عند إيقاف JavaScript
- تشغيل الاختبارات المعيارية في عملية التكامل المستمر
- الإطارات المجمّدة
- إنشاء ملفات شخصية أساسية وقياسها بدون Macrobenchmark