有助提升 SQLite 性能的最佳实践(视图)

概念和 Jetpack Compose 实现

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
  }
}

只读取您需要的列

避免选择不需要的列,否则可能会降低查询速度并浪费资源。请只选择所使用的列。

在以下示例中,您选择了 idnamephone

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:将某个列中的所有数值相加。
  • MINMAX:确定最小值或最大值。适用于数字列、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()
}