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

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

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

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

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

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

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

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

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

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

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

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

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

להקל על מצב הסנכרון

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

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

Kotlin

db.execSQL("PRAGMA synchronous = NORMAL")

Java

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

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

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

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

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

יצירת אינדקסים עם כמה עמודות

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

לדוגמה, בהתאם לשאילתה הבאה:

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);
    ...
  }
}

הוספת פרמטרים לשאילתות באמצעות כרטיסי SQL, ולא באמצעות שרשור מחרוזות

מחרוזת השאילתה עשויה לכלול פרמטר שידוע רק בזמן הריצה, כמו:

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: מחרוזת מחרוזות באמצעות מפריד אופציונלי.

צריך להשתמש ב-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 Analyzer

ב-SQLite יש ממשק שורת פקודה (CLI) בשם 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"
    }
  }
}