שיטות מומלצות לביצועי SQLite

‫Android מציעה תמיכה מובנית ב-SQLite, מסד נתונים יעיל של SQL. כדי לשפר את הביצועים של האפליקציה ולוודא שהיא תישאר מהירה וצפויה גם כשהנתונים יגדלו, כדאי לפעול לפי השיטות המומלצות הבאות. השימוש בשיטות המומלצות האלה גם מצמצם את הסיכוי להיתקל בבעיות בביצועים שקשה לשחזר ולפתור.

כדי לשפר את הביצועים, כדאי לפעול לפי העקרונות הבאים:

  • קריאה של פחות שורות ועמודות: כדאי לבצע אופטימיזציה של השאילתות כדי לאחזר רק את הנתונים הדרושים. כדאי למזער את כמות הנתונים שנקראים ממסד הנתונים, כי אחזור של עודף נתונים עלול להשפיע על הביצועים.

  • העברת עבודה למנוע SQLite: ביצוע חישובים, סינון ומיון של פעולות בשאילתות SQL. שימוש במנוע השאילתות של SQLite יכול לשפר משמעותית את הביצועים.

  • שינוי סכימת מסד הנתונים: עיצוב סכימת מסד הנתונים כדי לעזור ל-SQLite ליצור תוכניות שאילתות וייצוגי נתונים יעילים. כדאי ליצור אינדקסים בטבלאות בצורה נכונה ולבצע אופטימיזציה של מבני הטבלאות כדי לשפר את הביצועים.

בנוסף, אתם יכולים להשתמש בכלים הזמינים לפתרון בעיות כדי למדוד את הביצועים של מסד הנתונים של SQLite ולזהות תחומים שצריך לבצע בהם אופטימיזציה.

מומלץ להשתמש בספריית Jetpack Room.

הגדרת מסד הנתונים לביצועים

כדי להגדיר את מסד הנתונים לביצועים אופטימליים ב-SQLite, צריך לפעול לפי השלבים שמפורטים בקטע הזה.

הפעלת רישום מראש ביומן

ב-SQLite, המערכת מטמיעה שינויים על ידי הוספה שלהם ליומן, שדוחסים אותו מדי פעם למסד הנתונים. התהליך הזה נקרא רישום מראש ביומן (WAL).

מפעילים את WAL, אלא אם משתמשים ב-ATTACH DATABASE.

הפחתת ההגבלה של מצב הסנכרון

כשמשתמשים ב-WAL, כברירת מחדל כל פעולת commit מנפיקה fsync כדי לוודא שהנתונים מגיעים לדיסק. כך משפרים את עמידות הנתונים, אבל תהליך השמירה של השינויים מתבצע לאט יותר.

ב-SQLite יש אפשרות לשלוט במצב הסינכרוני. אם מפעילים את WAL, צריך להגדיר את המצב הסינכרוני ל-NORMAL:

Kotlin

db.execSQL("PRAGMA synchronous = NORMAL")

Java

db.execSQL("PRAGMA synchronous = NORMAL");

בהגדרה הזו, פעולת commit יכולה להסתיים לפני שהנתונים נשמרים בדיסק. אם מתרחשת כיבוי של המכשיר, למשל בגלל הפסקת חשמל או פאניקה בקרנל, יכול להיות שהנתונים שנשמרו יאבדו. עם זאת, בגלל הרישום ביומן, מסד הנתונים לא ניזוק.

אם רק האפליקציה קורסת, הנתונים עדיין מגיעים לדיסק. ברוב האפליקציות, ההגדרה הזו משפרת את הביצועים ללא עלות משמעותית.

הגדרת סכימות יעילות של טבלאות

כדי לשפר את הביצועים ולצמצם את צריכת הנתונים, צריך להגדיר סכימת טבלה יעילה. ‫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 או בקובץ, ואז לאחסן את נתיב הקובץ בעמודה.

בדרך כלל, הגודל של הקבצים מעוגל כלפי מעלה למכפלות של 4KB. בקובצי נתונים קטנים מאוד, שבהם שגיאת העיגול משמעותית, עדיף לאחסן אותם במסד הנתונים כ-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 כדי להחזיר תוצאות נפרדות. הלולאה התוכניתית איטית פי 1,000 משאילתת 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: שרשור מחרוזות עם מפריד אופציונלי.

במקום Cursor.getCount(), צריך להשתמש ב-COUNT()

בדוגמה הבאה, הפונקציה 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);
  ...
}

הוספת שאילתות במקום קוד

‫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 Browser

אפשר גם להתקין את כלי ה-GUI‏ 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

כשמגדירים את 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: הגודל של דף אחד במסד הנתונים, ב-KB.
  • dbsz: הגודל של כל מסד הנתונים, בדפים. כדי לקבל את הגודל ב-KB, מכפילים את pgsz ב-dbsz.
  • Lookaside(b): הזיכרון שהוקצה למאגר lookaside של SQLite לכל חיבור, בבייטים. הן בדרך כלל קטנות מאוד.
  • cache hits: ‏ SQLite מתחזק מטמון של דפי מסד נתונים. זהו מספר ההתאמות במטמון הדפים (ספירה).
  • cache misses: מספר הפעמים שבהן לא נמצאו נתונים במטמון של הדף (ספירה).
  • cache size: מספר הדפים במטמון (ספירה). כדי לקבל את הגודל ב-KB, מכפילים את המספר הזה ב-pgsz.
  • Dbname: הנתיב לקובץ DB. בדוגמה שלנו, לחלק ממסדי הנתונים מצורף המספר (1) או מספר אחר, כדי לציין שיש יותר מחיבור אחד לאותו מסד נתונים בסיסי. הנתונים הסטטיסטיים נמדדים לפי חיבור.

בקטע POOL STATS מופיעים:

  • cache hits: ‏ SQLite שומר במטמון הצהרות מוכנות ומנסה לעשות בהן שימוש חוזר כשמריצים שאילתות, כדי לחסוך מאמץ וזיכרון בהידור של הצהרות SQL. זה מספר ההיטים במטמון של ההצהרות (ספירה).
  • cache misses: מספר הפעמים שבהן לא נמצאה הצהרה במטמון (ספירה).
  • cache size: החל מ-Android 17, כאן מפורט המספר הכולל של ההצהרות המוכנות במטמון. בגרסאות קודמות, הערך הזה שווה לסכום של הפגיעות וההחטאות שמופיעים בשתי העמודות האחרות, והוא לא מייצג את גודל המטמון.