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