Các khái niệm và cách triển khai Jetpack Compose
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.
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
// 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");
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.
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()) {
// Process cursor data
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""", null)) {
while (cursor.moveToNext()) {
// Process cursor data
}
}
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, name và 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
}
}
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)
// Further processing
}
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT name
FROM Customers;
""", null)) {
while (cursor.moveToNext()) {
String name = cursor.getString(0);
// Further processing
}
}
Tham số hoá truy vấn
Chuỗi truy vấn của bạn có thể bao gồm một tham số chỉ được biết tại 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 đoạn mã trước, mỗi truy vấn đều tạo một chuỗi khác nhau, do đó không được hưởng lợi từ bộ nhớ đệm câu lệnh. Mỗi lệnh gọi đều 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, truy vấn có thể được biên dịch một lần và lưu vào bộ nhớ đệm. Truy vấn đã biên dịch được dùng lại giữa các lệnh gọi getNameById(long) khác nhau.
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
// 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
}
}
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
// 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
}
}
Để 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
// 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
}
}
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.MINhoặcMAX: 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ạiDATEvà 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()
// 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
}
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)
// Use count
}
Java
try (Cursor cursor = db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""", null)) {
cursor.moveToFirst();
int count = cursor.getInt(0);
// Use count
}
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()) {
// 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
}
}
}
}
Để 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()) {
// 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
}
}
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, "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()
}
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Chạy phép đo điểm chuẩn trong tính năng Tích hợp liên tục
- Khung hình bị treo
- Tạo và đo lường Hồ sơ cơ sở mà không cần Macrobenchmark