מושגים והטמעה של Jetpack Compose
ב-Android יש תמיכה מובנית ב-SQLite, מסד נתונים יעיל של SQL. כדאי לפעול לפי השיטות המומלצות הבאות כדי לשפר את ביצועי האפליקציה, ולוודא שהיא תמשיך לפעול במהירות גם כשהנתונים יגדלו. השיטות המומלצות האלה גם עוזרות לצמצם את הסיכוי להיתקל בבעיות בביצועים שקשה לשחזר ולפתור.
כדי לשפר את הביצועים, כדאי לפעול לפי העקרונות הבאים:
קריאה של פחות שורות ועמודות: מבצעים אופטימיזציה של השאילתות כדי לאחזר רק את הנתונים הנחוצים. לצמצם את כמות הנתונים שנקראים ממסד הנתונים, כי אחזור נתונים עודפים יכול להשפיע על הביצועים.
העברת העבודה למנוע SQLite: מבצעים פעולות חישוב, סינון ומיון בתוך שאילתות ה-SQL. שימוש במנוע השאילתות של SQLite יכול לשפר משמעותית את הביצועים.
שינוי הסכימה של מסד הנתונים: תכננו את הסכימה של מסד הנתונים כך שתעזור ל-SQLite ליצור תוכניות לביצוע שאילתה יעילות וייצוגי נתונים. כדי לשפר את הביצועים, חשוב להוסיף לאינדקס טבלאות בצורה נכונה ולבצע אופטימיזציה של מבני הטבלאות.
בנוסף, אתם יכולים להשתמש בכלים הזמינים לפתרון בעיות כדי למדוד את הביצועים של מסד הנתונים ב-SQLite, וכך לזהות תחומים שצריך לבצע בהם אופטימיזציה.
מומלץ להשתמש בספריית Jetpack Room.
הגדרת מסד הנתונים לביצועים מיטביים
כדי להגדיר את מסד הנתונים לביצועים אופטימליים ב-SQLite, פועלים לפי השלבים שמפורטים בקטע הזה.
הקלה על מצב הסנכרון
ב-SQLite יש אפשרות לשלוט במצב הסינכרוני. במצב FULL, כל commit מנפיק fsync כדי לוודא שהנתונים מגיעים לדיסק. כך משפרים את עמידות הנתונים, אבל מאטים את ה-commit. עם WAL, מצב NORMAL לא מסתנכרן בכל commit ועדיין מגן על מסד הנתונים מפני פגיעה.
כשמפעילים את WAL דרך ה-framework, מערכת Android מחילה את המצב הסינכרוני של WAL שמוגדר כברירת מחדל במכשיר, שהוא NORMAL ב-AOSP ל-Android מגרסה 9 ואילך.
יצרני המכשירים יכולים לשנות את ברירת המחדל הזו, ומסדי נתונים שלא משתמשים ב-WAL מוגדרים כברירת מחדל ל-FULL. כדי לוודא שמסד הנתונים משתמש ב-NORMAL, צריך להגדיר אותו כשפותחים את מסד הנתונים:
Kotlin
// When opening the database
val params = SQLiteDatabase.OpenParams.Builder()
.addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
// SYNC_MODE_NORMAL is available in API level 33 and higher.
// On API levels 28 to 32, pass "NORMAL" instead.
.setSynchronousMode(SQLiteDatabase.SYNC_MODE_NORMAL)
.build()
// With SQLiteOpenHelper, call setOpenParams(params) in your constructor.
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL")
Java
// When opening the database
SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder()
.addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
// SYNC_MODE_NORMAL is available in API level 33 and higher.
// On API levels 28 to 32, pass "NORMAL" instead.
.setSynchronousMode(SQLiteDatabase.SYNC_MODE_NORMAL)
.build();
// With SQLiteOpenHelper, call setOpenParams(params) in your constructor.
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");
בהגדרה הזו, יכול להיות שפעולת commit תוחזר לפני שהנתונים יאוחסנו בדיסק. אם המכשיר נכבה, למשל בגלל הפסקת חשמל או תגובה לשגיאת ליבה קריטית, יכול להיות שהנתונים שנשמרו יאבדו. עם זאת, בגלל הרישום ביומן, מסד הנתונים לא נפגם.
אם רק האפליקציה קורסת, הנתונים עדיין מגיעים לדיסק. ברוב האפליקציות, ההגדרה הזו משפרת את הביצועים ללא עלות משמעותית.
שיפור ביצועי השאילתות
כדי לשפר את ביצועי השאילתות ב-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: משרשר מחרוזות עם מפריד אופציונלי.
שימוש ב-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()
// 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")
)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
Java
db.beginTransaction();
try {
for (Customer customer : customers) {
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
new String[] {
String.valueOf(customer.id),
customer.name,
"customerValue"
});
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- הרצת נקודות השוואה באינטגרציה רציפה (CI)
- פריימים קפואים
- יצירה ומדידה של פרופילי Baseline בלי Macrobenchmark