מושגים ויישום ב-Jetpack Compose
Android מציעה תמיכה מובנית ב-SQLite, מסד נתונים יעיל של SQL. כדי לשפר את הביצועים של האפליקציה ולוודא שהיא תישאר מהירה גם כשהנתונים יגדלו, כדאי לפעול לפי השיטות המומלצות הבאות. בנוסף, השימוש בשיטות המומלצות האלה מפחית את הסיכוי להיתקל בבעיות בביצועים שקשה לשחזר ולפתור.
כדי לשפר את הביצועים, כדאי לפעול לפי העקרונות הבאים:
קריאה של פחות שורות ועמודות: כדאי לבצע אופטימיזציה של השאילתות כדי לאחזר רק את הנתונים הדרושים. כדאי לצמצם את כמות הנתונים שנקראים ממסד הנתונים, כי אחזור של עודף נתונים עלול להשפיע על הביצועים.
העברת עבודה למנוע SQLite: ביצוע חישובים, סינון ומיון של פעולות בשאילתות SQL. שימוש במנוע השאילתות של SQLite יכול לשפר באופן משמעותי את הביצועים.
שינוי סכימת מסד הנתונים: עיצוב סכימת מסד הנתונים כדי לעזור ל-SQLite ליצור תוכניות יעילות לשאילתות ולייצוגי נתונים. כדאי ליצור אינדקסים לטבלאות בצורה נכונה ולבצע אופטימיזציה של מבני הטבלאות כדי לשפר את הביצועים.
בנוסף, אתם יכולים להשתמש בכלים לפתרון בעיות שזמינים כדי למדוד את הביצועים של מסד הנתונים של SQLite ולזהות תחומים שצריך לבצע בהם אופטימיזציה.
מומלץ להשתמש בספריית Jetpack Room.
הגדרת מסד הנתונים לביצועים
כדי להגדיר את מסד הנתונים לביצועים אופטימליים ב-SQLite, פועלים לפי השלבים שמפורטים בקטע הזה.
הגדרת שיטת סנכרון פחות מחמירה
כשמשתמשים ב-WAL, כברירת מחדל כל פעולת commit מנפיקה fsync כדי לוודא שהנתונים מגיעים לדיסק. כך משפרים את עמידות הנתונים, אבל מאטים את פעולות ה-commit.
ב-SQLite יש אפשרות לשלוט במצב הסינכרוני. אם מפעילים את WAL, צריך להגדיר את המצב הסינכרוני ל-NORMAL:
Kotlin
// When opening the database
val paramsBuilder: SQLiteDatabase.OpenParams.Builder = SQLiteDatabase.OpenParams.Builder()
paramsBuilder.journalMode = SQLiteDatabase.SYNC_MODE_NORMAL
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");
Java
// When opening the database
SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder();
paramsBuilder.setJournalMode(SQLiteDatabase.SYNC_MODE_NORMAL);
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");
בהגדרה הזו, אפשר להחזיר אישור לפני שהנתונים מאוחסנים בדיסק. אם מתרחש אירוע השבתה של המכשיר, למשל בגלל הפסקת חשמל או תגובה לשגיאת ליבה קריטית, יכול להיות שהנתונים שנשמרו יאבדו. עם זאת, בגלל הרישום ביומן, מסד הנתונים לא נפגם.
אם רק האפליקציה קורסת, הנתונים עדיין מגיעים לדיסק. ברוב האפליקציות, ההגדרה הזו משפרת את הביצועים ללא עלות משמעותית.
שיפור הביצועים של שאילתות
כדי לשפר את ביצועי השאילתות ב-SQLite, כדאי לפעול לפי השיטות המומלצות הבאות כדי לצמצם את זמני התגובה ולמקסם את יעילות העיבוד.
יכול להחזיר רק שורה אחת או אפס שורות.קריאה רק של השורות שצריך
המסננים מאפשרים לצמצם את התוצאות על ידי ציון קריטריונים מסוימים, כמו טווח תאריכים, מיקום או שם. ההגבלות מאפשרות לכם לקבוע את מספר התוצאות שיוצגו:
Kotlin
db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Process cursor data
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""", null)) {
while (cursor.moveToNext()) {
// Process cursor data
}
}
קריאה רק של העמודות שצריך
מומלץ להימנע מבחירה של עמודות לא נחוצות, כי זה עלול להאט את השאילתות ולבזבז משאבים. במקום זאת, בוחרים רק את העמודות שבהן משתמשים.
בדוגמה הבאה, בוחרים באפשרויות 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)
// Further processing
}
}
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);
// Further processing
}
}
עם זאת, צריך רק את העמודה name:
Kotlin
db.rawQuery("""
SELECT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(0)
// Further processing
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT name
FROM Customers;
""", null)) {
while (cursor.moveToNext()) {
String name = cursor.getString(0);
// Further processing
}
}
הוספת פרמטרים לשאילתות
מחרוזת השאילתה יכולה לכלול פרמטר שמוכר רק בזמן הריצה, כמו:
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).
שימוש במשתנה 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
// Process distinct name
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT DISTINCT name
FROM Customers;
""", null)) {
while (cursor.moveToNext()) {
// Only iterate over distinct names in Java
// Process distinct name
}
}
השתמשו בפונקציות צבירה כשהדבר אפשרי
להשתמש בפונקציות צבירה כדי לצבור תוצאות בלי נתוני שורות. לדוגמה, הקוד הבא בודק אם יש לפחות שורה אחת שתואמת:
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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
כדי לאחזר רק את השורה הראשונה, אפשר להשתמש ב-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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
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
// Handle found
} else {
// No customers from Paris
// Handle not found
}
}
שימוש בפונקציות מצטברות של 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()
// Use count
}
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();
// Use count
}
אבל אם משתמשים ב-COUNT(), מסד הנתונים מחזיר רק את המספר:
Kotlin
db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
cursor.moveToFirst()
val count = cursor.getInt(0)
// Use count
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""", null)) {
cursor.moveToFirst();
int count = cursor.getInt(0);
// Use count
}
הוספת שאילתות במקום קוד
אפשר להרכיב שאילתות 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()) {
// Process inner cursor data
}
}
}
}
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()) {
// Process inner cursor data
}
}
}
}
כדי לקבל את התוצאה בחצי מהזמן של הדוגמה הקודמת, אפשר להשתמש בשאילתת 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()) {
// Process cursor data
}
}
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()) {
// Process cursor data
}
}
בדיקת ייחודיות ב-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, "customerValue")
)
}
} finally {
db.endTransaction()
}
Java
db.beginTransaction();
try {
for (customer : Customers) {
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
new String[] {
String.valueOf(customer.id),
customer.name,
"customerValue"
});
}
} finally {
db.endTransaction()
}
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- הרצת בדיקות השוואה באינטגרציה רציפה
- פריימים קפואים
- יצירה ומדידה של פרופילי Baseline בלי Macrobenchmark