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

يوفر Android دعمًا مضمّنًا لـ SQLite، وهي قاعدة بيانات SQL فعالة. اتّبِع أفضل الممارسات التالية لتحسين أداء التطبيق، والتأكّد من الحفاظ على سرعته وسهولة توقّعه مع نمو بياناتك. من خلال اتّباع أفضل الممارسات هذه، يمكنك أيضًا الحدّ من احتمال مواجهة مشاكل في الأداء يصعب إعادة إنتاجها وتحديد المشاكل وحلّها.

لتحقيق أداء أسرع، اتّبِع مبادئ الأداء التالية:

  • قراءة عدد أقل من الصفوف والأعمدة: يمكنك تحسين طلبات البحث لاسترداد البيانات الضرورية فقط. قم بتقليل كمية البيانات المقروءة من قاعدة البيانات، لأن استرداد البيانات الزائد يمكن أن يؤثر على الأداء.

  • دفع العمل إلى محرك SQLite: يمكنك إجراء عمليات العمليات الحسابية والفلترة والترتيب ضمن طلبات بحث SQL. يمكن أن يؤدي استخدام محرك استعلام SQLite إلى تحسين الأداء بشكل كبير.

  • تعديل مخطط قاعدة البيانات: صمِّم مخطط قاعدة البيانات لمساعدة SQLite في إنشاء خطط طلبات بحث وتمثيلات فعّالة للبيانات. يمكنك فهرسة الجداول بشكل صحيح وتحسين هياكل الجداول لتحسين الأداء.

بالإضافة إلى ذلك، يمكنك استخدام الأدوات المتاحة لتحديد المشاكل وحلّها لقياس أداء قاعدة بيانات SQLite للمساعدة في تحديد المجالات التي تحتاج إلى تحسين.

ننصحك باستخدام مكتبة غرفة Jetpack.

ضبط قاعدة البيانات للأداء

اتبع الخطوات الواردة في هذا القسم لإعداد قاعدة البيانات لتحقيق الأداء الأمثل في SQLite.

تفعيل ميزة "تسجيل المكالمات مسبقًا"

ينفذ SQLite الطفرات من خلال إلحاقها بسجل، والذي يتم ضغطه أحيانًا في قاعدة البيانات. ويُطلَق على ذلك تسجيل الدخول المسبق (WAL).

تفعيل 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');

نتيجة الجدول هي على النحو التالي:

معرّف الصف 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:

مدينة معرّف الصف
غاري، إنديانا 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);
    ...
  }
}

استخدِم 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 واجهة سطر الأوامر (CLI) في sqlite3_analyzer للاطّلاع على معلومات إضافية يمكن استخدامها لتحديد المشاكل في الأداء وحلّها. للتثبيت، انتقل إلى صفحة تنزيل SQLite.

يمكنك استخدام adb pull لتنزيل ملف قاعدة بيانات من جهاز مستهدف إلى محطة العمل لتحليله:

adb pull /data/data/<app_package_name>/databases/<db_name>.db

متصفح SQLite

يمكنك أيضًا تثبيت أداة واجهة المستخدم الرسومية متصفّح SQLite على صفحة "عمليات التنزيل" في 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 tracing {:#perfetto-tracing}

When [configuring Perfetto](https://perfetto.dev/docs/concepts/config), you may
add the following to include tracks for individual queries:

```protobuf
data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      atrace_categories: "database"
    }
  }
}