أفضل الممارسات لتحسين أداء SQLite

يتيح نظام التشغيل 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، تعرض هذه السمة إجمالي عدد العبارات المُعدّة في ذاكرة التخزين المؤقت. في الإصدارات السابقة، كانت هذه القيمة تساوي مجموع النتائج الإيجابية والسلبية المدرَجة في العمودَين الآخرَين، ولا تمثّل حجم ذاكرة التخزين المؤقت.