Các phương pháp hay nhất về hiệu suất SQLite

Android cung cấp tính năng hỗ trợ tích hợp cho SQLite, một cơ sở dữ liệu SQL hiệu quả. Hãy làm theo các phương pháp hay nhất này để tối ưu hoá hiệu suất của ứng dụng, đảm bảo rằng khi dữ liệu của bạn phát triển thì ứng dụng vẫn chạy nhanh và dễ dự đoán. Bằng cách áp dụng các phương pháp hay nhất này, khả năng bạn gặp phải các vấn đề về hiệu suất và khắc phục sự cố sẽ giảm nhiều.

Để đạt được hiệu suất nhanh hơn, hãy làm theo các nguyên tắc về hiệu suất sau:

  • Đọc ít hàng và cột hơn: Tối ưu hoá các truy vấn của bạn để chỉ truy xuất dữ liệu cần thiết. Giảm lượng dữ liệu được đọc từ cơ sở dữ liệu, vì việc truy xuất dữ liệu quá mức có thể ảnh hưởng đến hiệu suất.

  • Đưa công việc lên công cụ SQLite: Thực hiện các phép tính, lọc và sắp xếp trong truy vấn SQL. Việc sử dụng công cụ truy vấn của SQLite có thể cải thiện đáng kể hiệu suất.

  • Sửa đổi giản đồ cơ sở dữ liệu: Hãy thiết kế giản đồ cơ sở dữ liệu để giúp SQLite xây dựng các kế hoạch truy vấn và thể hiện dữ liệu hiệu quả. Hãy lập chỉ mục bảng đúng cách và tối ưu hoá cấu trúc bảng để nâng cao hiệu suất.

Ngoài ra, bạn có thể sử dụng các công cụ khắc phục sự cố hiện có để đo lường hiệu suất của cơ sở dữ liệu SQLite nhằm xác định những khía cạnh cần tối ưu hoá.

Bạn nên dùng thư viện Jetpack Room.

Định cấu hình cơ sở dữ liệu nhằm đạt được hiệu suất

Hãy làm theo các bước trong phần này để định cấu hình cơ sở dữ liệu nhằm đạt được hiệu suất tối ưu trong SQLite.

Bật tính năng Ghi nhật ký trước

SQLite triển khai các phép biến đổi bằng cách thêm chúng vào nhật ký mà đôi khi sẽ nén vào cơ sở dữ liệu. Tính năng này được gọi là Ghi nhật ký trước (Write-Ahead Logging – WAL).

Hãy bật tính năng WAL, trừ phi bạn sử dụng ATTACH DATABASE.

Nới lỏng chế độ đồng bộ hoá

Theo mặc định, khi sử dụng WAL, mọi lệnh xác nhận đều đưa ra một fsync để đảm bảo rằng dữ liệu đến được ổ đĩa. Điều này giúp dữ liệu khó bị hỏng hơn, nhưng làm chậm lệnh xác nhận.

SQLite cho phép bạn lựa chọn kiểm soát chế độ đồng bộ. Nếu bạn bật WAL, hãy thiết lập chế độ đồng bộ thành NORMAL:

Kotlin

db.execSQL("PRAGMA synchronous = NORMAL")

Java

db.execSQL("PRAGMA synchronous = NORMAL");

Trong chế độ cài đặt này, một lệnh xác nhận có thể trả về trước khi dữ liệu được lưu trữ trong ổ đĩa. Trong trường hợp thiết bị bị tắt, chẳng hạn như mất điện hoặc hoảng loạn hạt nhân, thì dữ liệu đã xác nhận có thể bị mất. Tuy nhiên, nhờ việc ghi nhật ký, cơ sở dữ liệu của bạn sẽ không bị hỏng.

Nếu chỉ có ứng dụng của bạn gặp sự cố, dữ liệu của bạn vẫn sẽ đến được ổ đĩa. Đối với hầu hết ứng dụng, chế độ cài đặt này giúp cải thiện hiệu suất mà không hao tổn tài nguyên.

Xác định giản đồ bảng hiệu quả

Để tối ưu hoá hiệu suất và giảm thiểu mức sử dụng dữ liệu, hãy xác định một giản đồ bảng hiệu quả. SQLite xây dựng các kế hoạch và dữ liệu để truy vấn hiệu quả, giúp truy xuất dữ liệu nhanh hơn. Phần này cung cấp các phương pháp hay nhất để tạo giản đồ bảng.

Cân nhắc sử dụng INTEGER PRIMARY KEY

Đối với ví dụ này, hãy xác định và điền vào bảng như sau:

CREATE TABLE Customers(
  id INTEGER,
  name TEXT,
  city TEXT
);
INSERT INTO Customers Values(456, 'John Lennon', 'Liverpool, England');
INSERT INTO Customers Values(123, 'Michael Jackson', 'Gary, IN');
INSERT INTO Customers Values(789, 'Dolly Parton', 'Sevier County, TN');

Kết quả của bảng như sau:

rowid id tên thành phố
1 456 John Lennon Liverpool, Anh
2 123 Michael Jackson Gary, Indiana
3 789 Dolly Parton Quận Sevier, TN

Cột rowid là một chỉ mục lưu giữ thứ tự chèn. Các truy vấn lọc theo rowid sẽ được triển khai dưới dạng tìm kiếm cây B nhanh, nhưng các truy vấn lọc theo id sẽ được triển khai dưới dạng quét bảng chậm.

Nếu dự định thực hiện tra cứu theo id, bạn có thể tránh lưu trữ cột rowid để có ít dữ liệu hơn trong bộ nhớ và nhanh hơn về cơ sở dữ liệu tổng thể:

CREATE TABLE Customers(
  id INTEGER PRIMARY KEY,
  name TEXT,
  city TEXT
);

Bây giờ, bảng của bạn sẽ có dạng như sau:

id tên thành phố
123 Michael Jackson Gary, Indiana
456 John Lennon Liverpool, Anh
789 Dolly Parton Quận Sevier, TN

Vì bạn không cần lưu trữ cột rowid nên truy vấn id sẽ có tốc độ nhanh. Lưu ý rằng bảng hiện được sắp xếp dựa trên id thay vì thứ tự chèn.

Tăng tốc truy vấn bằng chỉ mục

Sử dụng SQLite chỉ mục để tăng tốc độ truy vấn. Khi lọc (WHERE), sắp xếp (ORDER BY) hoặc tổng hợp (GROUP BY) một cột, nếu bảng có chỉ mục cho cột, thì truy vấn sẽ được tăng tốc.

Trong ví dụ trước, việc lọc theo city yêu cầu phải quét toàn bộ bảng:

SELECT id, name
WHERE city = 'London, England';

Đối với ứng dụng có nhiều truy vấn theo "thành phố", bạn có thể tăng tốc các truy vấn đó bằng một chỉ mục:

CREATE INDEX city_index ON Customers(city);

Chỉ mục sẽ được triển khai dưới dạng bảng bổ sung, được sắp xếp theo cột chỉ mục và được ánh xạ tới rowid:

thành phố rowid
Gary, Indiana 2
Liverpool, Anh 1
Quận Sevier, TN 3

Xin lưu ý rằng dung lượng lưu trữ của cột city hiện hao tổn gấp đôi vì có trong cả bảng ban đầu và chỉ mục. Vì bạn đang sử dụng chỉ mục, mức hao tổn thêm dung lượng lưu trữ sẽ đánh đổi bằng việc truy vấn nhanh hơn. Tuy nhiên, không nên duy trì chỉ mục mà bạn không dùng để tránh phải hao tổn dung lượng lưu trữ mà không đạt được hiệu suất truy vấn.

Tạo chỉ mục nhiều cột

Nếu truy vấn của bạn kết hợp nhiều cột, bạn có thể tạo chỉ mục nhiều cột để hoàn toàn tăng tốc độ truy vấn. Bạn cũng có thể sử dụng chỉ mục trên cột bên ngoài và cho phép cột bên trong thực hiện tìm kiếm dưới dạng quét lần lượt từng mục.

Ví dụ: với truy vấn sau:

SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name

Bạn có thể tăng tốc truy vấn bằng chỉ mục nhiều cột theo thứ tự được chỉ định trong truy vấn:

CREATE INDEX city_name_index ON Customers(city, name);

Tuy nhiên, nếu bạn chỉ có một chỉ mục trên city, thì thứ tự bên ngoài vẫn được tăng tốc, trong khi thứ tự bên trong cần phải được quét lần lượt từng mục.

Điều này cũng áp dụng với các truy vấn có tiền tố. Ví dụ: chỉ mục ON Customers (city, name) cũng tăng tốc độ lọc, sắp xếp và nhóm theo city, vì bảng chỉ mục cho chỉ mục nhiều cột được sắp xếp theo thứ tự nhất định của các chỉ số có sẵn.

Cân nhắc sử dụng WITHOUT ROWID

Theo mặc định, SQLite sẽ tạo một cột rowid cho bảng của bạn, trong đó rowidINTEGER PRIMARY KEY AUTOINCREMENT ngầm ẩn. Nếu bạn đã có một cột là INTEGER PRIMARY KEY, thì cột này sẽ trở thành bí danh của rowid.

Đối với các bảng có khoá chính khác INTEGER hoặc một tổ hợp gồm nhiều cột, hãy cân nhắc sử dụng WITHOUT ROWID.

Lưu trữ dữ liệu nhỏ dưới dạng BLOB và lưu trữ dữ liệu lớn dưới dạng tệp

Nếu muốn liên kết dữ liệu lớn (ví dụ: hình thu nhỏ của một hình ảnh hoặc ảnh của một người liên hệ) với một hàng, bạn có thể lưu trữ dữ liệu đó trong cột BLOB hoặc trong một tệp và sau đó lưu trữ đường dẫn tệp trong cột.

Các tệp thường được làm tròn lên đến 4 KB. Đối với các tệp rất nhỏ có lỗi làm tròn đáng kể, sẽ hiệu quả hơn nếu bạn lưu trữ các tệp này trong cơ sở dữ liệu dưới dạng BLOB. Trong một số trường hợp, SQLite sẽ giảm thiểu các lệnh gọi hệ thống tệp và nhanh hơn hệ thống tệp cơ sở.

Cải thiện hiệu suất truy vấn

Hãy làm theo các phương pháp hay nhất này để cải thiện hiệu suất truy vấn trong SQLite bằng cách giảm thiểu thời gian phản hồi và tối đa hoá hiệu quả xử lý.

Chỉ đọc những hàng cần thiết

Bộ lọc cho phép bạn thu hẹp kết quả của mình bằng cách chỉ định các tiêu chí nhất định, chẳng hạn như phạm vi ngày, vị trí, hoặc tên. Giới hạn cho phép bạn kiểm soát số lượng kết quả bạn thấy:

Kotlin

db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        ...
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """, null)) {
  while (cursor.moveToNext()) {
    ...
  }
}

Chỉ đọc những cột cần thiết

Tránh chọn các cột không cần thiết vì có thể làm chậm tốc độ truy vấn và lãng phí tài nguyên. Thay vào đó, chỉ chọn những cột được sử dụng.

Trong ví dụ sau, bạn sẽ chọn id, namephone:

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)
        // ...
    }
}

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);
    ...
  }
}

Tuy nhiên, bạn chỉ cần cột name:

Kotlin

db.rawQuery("""
    SELECT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        val name = cursor.getString(0)
        ...
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    String name = cursor.getString(0);
    ...
  }
}

Tham số truy vấn bằng Thẻ SQL, không phải bằng cách nối chuỗi

Chuỗi truy vấn của bạn có thể bao gồm một tham số chỉ được biết trong thời gian chạy, chẳng hạn như sau:

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

Trong mã trên, mỗi truy vấn đều xây dựng một chuỗi khác nhau và do đó không được hưởng lợi từ bộ nhớ đệm của câu lệnh. Mỗi lệnh gọi yêu cầu SQLite biên dịch lệnh gọi đó trước khi có thể thực thi. Thay vào đó, bạn có thể thay thế đối số id bằng một tham số và liên kết giá trị với 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;
    }
  }
}

Giờ đây, bạn có thể biên dịch truy vấn một lần và lưu vào bộ nhớ đệm. Truy vấn đã biên dịch được sử dụng lại giữa các lệnh gọi khác nhau của getNameById(long).

Lặp lại trong SQL, chứ không phải trong mã

Sử dụng một truy vấn duy nhất trả về tất cả kết quả được nhắm mục tiêu, thay vì truy vấn có lập trình lặp lại trên các truy vấn SQL để trả về từng kết quả riêng lẻ. Vòng lặp có lập trình chậm hơn khoảng 1000 lần so với một truy vấn SQL.

Sử dụng DISTINCT đối với các giá trị duy nhất

Việc sử dụng từ khoá DISTINCT có thể cải thiện hiệu suất truy vấn bằng cách giảm lượng dữ liệu cần xử lý. Ví dụ: nếu bạn chỉ muốn trả về các giá trị duy nhất từ một cột, hãy sử dụng DISTINCT:

Kotlin

db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        // Only iterate over distinct names in Kotlin
        ...
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    // Only iterate over distinct names in Java
    ...
  }
}

Sử dụng các hàm tổng hợp khi có thể

Sử dụng các hàm tổng hợp đối với kết quả tổng hợp mà không cần dữ liệu hàng. Ví dụ: đoạn mã sau đây sẽ kiểm tra xem có ít nhất một hàng phù hợp không:

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
        ...
    } else {
        // No customers from Paris
        ...
}

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
    ...
  } else {
    // No customers from Paris
    ...
  }
}

Để chỉ tìm nạp hàng đầu tiên, bạn có thể sử dụng EXISTS() để trả về 0 nếu không có hàng phù hợp và 1 nếu có một hoặc nhiều hàng trùng khớp:

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
        ...
    } else {
        // No customers from Paris
        ...
    }
}

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
    ...
  } else {
    // No customers from Paris
    ...
  }
}

Sử dụng các hàm tổng hợp SQLite trong mã lập trình ứng dụng của bạn:

  • COUNT: đếm xem có bao nhiêu hàng trong một cột.
  • SUM: thêm tất cả giá trị bằng số vào cột.
  • MIN hoặc MAX: xác định giá trị thấp nhất hoặc cao nhất. Hoạt động với các cột số, loại DATE và loại văn bản.
  • AVG: tìm giá trị số trung bình.
  • GROUP_CONCAT: nối các chuỗi với dấu phân tách tuỳ chọn.

Sử dụng COUNT() thay vì Cursor.getCount()

Trong ví dụ sau, hàm Cursor.getCount() đọc tất cả hàng trong cơ sở dữ liệu và trả về tất cả giá trị hàng:

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

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();
  ...
}

Tuy nhiên, bằng cách sử dụng COUNT(), cơ sở dữ liệu chỉ trả về số đếm:

Kotlin

db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    cursor.moveToFirst()
    val count = cursor.getInt(0)
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """, null)) {
  cursor.moveToFirst();
  int count = cursor.getInt(0);
  ...
}

Dùng truy vấn Nest thay vì dùng đoạn mã

SQL có thể kết hợp và hỗ trợ các truy vấn con, tham số kết hợp và quy tắc ràng buộc đối với khoá foreign. Bạn có thể sử dụng kết quả của một truy vấn trong một truy vấn khác mà không xem mã lập trình ứng dụng. Điều này làm giảm nhu cầu sao chép dữ liệu từ SQLite và cho phép công cụ cơ sở dữ liệu tối ưu hoá truy vấn.

Trong ví dụ sau, bạn có thể chạy một truy vấn để tìm xem thành phố nào có nhiều khách hàng nhất, sau đó sử dụng kết quả trong một truy vấn khác để tìm tất cả khách hàng từ thành phố đó:

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()) {
                ...
            }
        }
    }
}

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()) {
          ...
        }
    }
  }
}

Để nhận được kết quả trong khoảng thời gian bằng phân nửa so với ví dụ trước, hãy sử dụng một truy vấn SQL với các câu lệnh lồng nhau:

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()) {
        ...
    }
}

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()) {
    ...
  }
}

Kiểm tra tính duy nhất trong SQL

Nếu không được chèn một hàng, trừ phi một giá trị cột cụ thể là duy nhất trong bảng, thì việc thực thi tính duy nhất đó dưới dạng một quy tắc hạn chế đối với cột có thể hiệu quả hơn.

Trong ví dụ sau, một truy vấn được chạy để xác thực hàng sẽ được chèn và một truy vấn khác thực sự chèn hàng đó vào:

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,
    });

Thay vì kiểm tra quy tắc ràng buộc riêng trong Kotlin hoặc Java, bạn có thể kiểm tra quy tắc ràng buộc này trong SQL khi xác định bảng:

CREATE TABLE Customers(
  id INTEGER PRIMARY KEY,
  name TEXT,
  username TEXT UNIQUE
);

SQLite thực hiện tương tự như sau:

CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);

Hiện tại, bạn có thể chèn một hàng và để SQLite kiểm tra quy tắc ràng buộc:

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 hỗ trợ các chỉ mục duy nhất với nhiều cột:

CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);

SQLite sẽ xác thực các quy tắc ràng buộc nhanh hơn và có mức hao tổn thấp hơn so với mã Kotlin hoặc Java. Bạn nên sử dụng SQLite thay vì mã ứng dụng.

Tạo nhiều lượt chèn trong một giao dịch

Một giao dịch sẽ thực hiện nhiều thao tác, giúp cải thiện không chỉ hiệu quả mà còn cả độ chính xác. Để cải thiện tính nhất quán của dữ liệu và tăng tốc hiệu suất, bạn có thể chèn hàng loạt:

Kotlin

db.beginTransaction()
try {
    customers.forEach { customer ->
        db.execSql(
            "INSERT INTO Customers VALUES (?, ?, ...)",
            arrayOf(customer.id.toString(), customer.name, ...)
        )
    }
} finally {
    db.endTransaction()
}

Java

db.beginTransaction();
try {
  for (customer : Customers) {
    db.execSQL(
        "INSERT INTO Customers VALUES (?, ?, ...)",
        new String[] {
          String.valueOf(customer.id),
          customer.name,
          ...
        });
  }
} finally {
  db.endTransaction()
}

Sử dụng công cụ khắc phục sự cố

SQLite cung cấp các công cụ khắc phục sự cố sau đây để giúp đo lường hiệu suất.

Sử dụng lời nhắc tương tác của SQLite

Chạy SQLite trên máy của bạn để chạy truy vấn và tìm hiểu. Các phiên bản nền tảng Android khác nhau sẽ sử dụng các bản sửa đổi khác nhau của SQLite. Để sử dụng cùng một công cụ trên thiết bị chạy Android, hãy sử dụng adb shell và chạy sqlite3 trên thiết bị mục tiêu.

Bạn có thể yêu cầu SQLite truy vấn theo thời gian:

sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...

EXPLAIN QUERY PLAN

Bạn có thể yêu cầu SQLite giải thích ý định trả lời truy vấn bằng cách sử dụng EXPLAIN QUERY PLAN:

sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers

Ví dụ trước yêu cầu quét toàn bộ bảng mà không có chỉ mục để tìm tất cả khách hàng sống ở Paris. Đây gọi là độ phức tạp tuyến tính. SQLite cần đọc tất cả hàng và chỉ giữ lại những hàng khớp với khách hàng ở Paris. Để khắc phục điều này, bạn có thể thêm một chỉ mục:

sqlite> CREATE INDEX Idx1 ON Customers(city);
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SEARCH test USING INDEX Idx1 (city=?

Nếu đang sử dụng shell tương tác, bạn có thể yêu cầu SQLite luôn giải thích kế hoạch truy vấn:

sqlite> .eqp on

Để biết thêm thông tin, hãy xem phần Lập kế hoạch truy vấn.

Trình phân tích SQLite

SQLite cung cấp sqlite3_analyzer giao diện dòng lệnh (CLI) để kết xuất thông tin bổ sung có thể được dùng để khắc phục sự cố hiệu suất. Để cài đặt, hãy truy cập trang Tải xuống của SQLite.

Bạn có thể sử dụng adb pull để tải tệp cơ sở dữ liệu từ một thiết bị mục tiêu xuống máy trạm để phân tích:

adb pull /data/data/<app_package_name>/databases/<db_name>.db

Trình duyệt SQLite

Bạn cũng có thể cài đặt công cụ GUI của Trình duyệt SQLite trên trang Tải xuống của SQLite.

Ghi nhật ký Android

Android đếm số lần truy vấn SQLite và ghi lại các truy vấn đó cho bạn:

# Enable query time logging
$ adb shell setprop log.tag.SQLiteTime VERBOSE
# Disable query time logging
$ adb shell setprop log.tag.SQLiteTime ERROR
```### Perfetto tracing

### Perfetto tracing {:#perfetto-tracing}

When [configuring Perfetto](https://perfetto.dev/docs/concepts/config), you may
add the following to include tracks for individual queries:

```protobuf
data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      atrace_categories: "database"
    }
  }
}