أفضل الممارسات لتحسين أداء SQLite (طرق العرض)

مفاهيم وتنفيذ Jetpack Compose

يتيح 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()
}