SQLite 是高效的 SQL 資料庫,而 Android 內建 SQLite 支援功能。只要採用下列最佳做法,就能充分提升應用程式效能,確保即使資料量增加,應用程式仍能維持快速穩定的表現。透過這些最佳做法,您也可以減少遇到效能問題的可能性,畢竟這類問題不容易重現及排解。
如要加快運作效能,請依循下列效能原則:
減少讀取的資料列/欄數量:以最佳方式查詢內容,僅擷取必要資料。請盡可能減少從資料庫讀取的資料量,因為過度擷取資料可能會影響效能。
將工作推送至 SQLite 引擎:在 SQL 查詢中執行運算、篩選和排序作業。使用 SQLite 的查詢引擎可大幅提升效能。
修改資料庫結構定義:妥善設計資料庫的結構定義,讓 SQLite 建構有效的查詢計畫和資料表示法。請為資料表建立適當索引,並改善資料表結構,進而提升效能。
此外,還能使用我們提供的疑難排解工具評估 SQLite 資料庫的效能,找出需要最佳化的部分。
建議使用 Jetpack Room 程式庫。
設定資料庫以提升效能
請按照本節步驟設定資料庫,進而在 SQLite 中獲得最佳效能。
放寬同步處理模式
使用 WAL 功能時,根據預設,每個修訂版本都會發出 fsync,確保資料能傳送至磁碟。這可提升資料耐用性,但會拖慢修訂版本執行速度。
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:以選用分隔符串連字串。
以 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,
});
定義資料表時,您可以在 SQL 中檢查限制的不重複值,而不用在 Kotlin 或 Java 中檢查:
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, ...);
相較於 Kotlin 或 Java 程式碼,SQLite 驗證限制條件的速度更快,且負擔較少。因此最佳做法是使用 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 關閉時顯示連結文字
- 在持續整合中執行基準測試
- 凍結影格
- 不使用 Macrobenchmark 建立及評估基準設定檔