SQLite 是一种高效的 SQL 数据库,Android 内置了对它的支持。遵循以下最佳实践可以优化您的应用的性能,确保随着数据的增多,您的应用仍能以可预测的速度快速运行。通过运用这些最佳实践,您还可以降低遇到难以重现且难以排查的性能问题的可能性。
要想实现更快的性能,请遵循以下性能原则:
减少读取的行数和列数:优化您的查询,以便仅检索必要的数据。最大限度地减少从数据库读取的数据量,因为过多的数据检索可能会影响性能。
将工作推送到 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 的情况下创建和衡量基准配置文件