Android, verimli bir SQL veritabanı olan SQLite için yerleşik destek sunar. Uygulamanızın performansını optimize etmek için bu en iyi uygulamaları takip edin. Böylece, verileriniz arttıkça uygulamanızın hızlı ve öngörülebilir şekilde hızlı kalmasını sağlayabilirsiniz. Bu en iyi uygulamaları kullanarak, yeniden oluşturulması ve sorunlarının giderilmesi zor olan performans sorunlarıyla karşılaşma olasılığını da azaltırsınız.
Daha hızlı performans elde etmek için aşağıdaki performans ilkelerini uygulayın:
Daha az satır ve sütun okuma: Sorgularınızı yalnızca gerekli verileri alacak şekilde optimize edin. Veritabanından okunan veri miktarını en aza indirin. Aşırı veri alma işlemi performansı etkileyebilir.
İşleri SQLite motoruna aktarın: SQL sorgularında hesaplama, filtreleme ve sıralama işlemleri yapın. SQLite'ın sorgu motorunu kullanmak performansı önemli ölçüde artırabilir.
Veritabanı şemasını değiştirme: Veritabanı şemanızı, SQLite'ın verimli sorgu planları ve veri temsilleri oluşturmasına yardımcı olacak şekilde tasarlayın. Performansı artırmak için tabloları doğru şekilde dizine ekleyin ve tablo yapılarını optimize edin.
Ayrıca, optimizasyon gerektiren alanları belirlemek için mevcut sorun giderme araçlarını kullanarak SQLite veritabanınızın performansını ölçebilirsiniz.
Jetpack Room kitaplığını kullanmanızı öneririz.
Veritabanını performans için yapılandırma
SQLite'ta optimum performans için veritabanınızı yapılandırmak üzere bu bölümdeki adımları uygulayın.
Write-Ahead Logging'i etkinleştirme
SQLite, mutasyonları bir günlüğe ekleyerek uygular ve bu günlüğü zaman zaman veritabanına sıkıştırır. Buna Write-Ahead Logging (WAL) denir.
ATTACH DATABASE kullanmıyorsanız WAL'yi etkinleştirin.
PRAGMA journal_mode=WAL komutunu kendiniz çalıştırmak yerine çerçeve üzerinden WAL'yi etkinleştirin. Room, düşük RAM'li olmayan cihazlarda WAL'yi varsayılan olarak etkinleştirir.
Room olmadan aşağıdakilerden birini kullanın:
enableWriteAheadLoggingyöntemiENABLE_WRITE_AHEAD_LOGGINGaçık işaretiSQLiteOpenHelperiçinsetWriteAheadLoggingEnabledyöntemi
Çerçeve, veritabanının WAL kullandığını bildiğinde okuma işlemlerinin yazma işlemiyle eşzamanlı olarak çalışabilmesi için bir bağlantı havuzu açar ve sonraki bölümde açıklanan WAL eşzamanlı modunu uygular. PRAGMA journal_mode=WAL komutunu kendiniz çalıştırırsanız çerçeve, veritabanını yine de geri alma günlüğü veritabanı olarak değerlendirir. Tek bir bağlantı kullanır ve commit'ler varsayılan FULL eşzamanlı modunu korur.
Senkronizasyon modunu gevşetme
SQLite'ta eşzamanlı modu kontrol etme seçeneği vardır. FULL modunda, her kaydetme işlemi, verilerin diske ulaşmasını sağlamak için bir fsync yayınlar. Bu, veri dayanıklılığını artırır ancak kaydetme işlemlerinizi yavaşlatır. WAL ile NORMAL modu her kayıtta senkronize edilmez ve veritabanını bozulmaya karşı korur.
WAL'yi çerçeve üzerinden etkinleştirdiğinizde Android, cihazın varsayılan WAL eşzamanlı modunu uygular. Bu mod, Android 9 ve sonraki sürümler için AOSP'de NORMAL şeklindedir.
Cihaz üreticileri bu varsayılanı değiştirebilir ve WAL kullanmayan veritabanları varsayılan olarak FULL değerini kullanır. Veritabanınızın NORMAL kullandığından emin olmak için veritabanını açarken bunu ayarlayın:
// When opening the database
val params = SQLiteDatabase.OpenParams.Builder()
.addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
// SYNC_MODE_NORMAL is available in API level 33 and higher.
// On API levels 28 to 32, pass "NORMAL" instead.
.setSynchronousMode(SQLiteDatabase.SYNC_MODE_NORMAL)
.build()
// With SQLiteOpenHelper, call setOpenParams(params) in your constructor.
// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL")
Bu ayarda, veriler bir diske depolanmadan önce bir commit döndürülebilir. Güç kaybı veya kernel panic gibi bir cihaz kapanması meydana gelirse kaydedilen veriler kaybolabilir. Ancak günlük kaydı sayesinde veritabanınız bozulmaz.
Yalnızca uygulamanız kilitlenirse verileriniz diske ulaşmaya devam eder. Çoğu uygulamada bu ayar, önemli bir maliyet olmadan performans iyileştirmeleri sağlar.
Bağlantıları ve hafızayı yönetme
Açık olan her veritabanı bağlantısı SQLite belleğini (sayfa önbelleği, lookaside ayırıcı ve derlenmiş ifadelerin önbelleği) tutar. Veritabanı dosyası sayfaları da çekirdek sayfa önbelleğinde kalır. Sorgularınızı yavaşlatmadan bu belleği kontrol altında tutmak için bu bölümdeki uygulamaları izleyin.
Her veritabanını bir kez açın
Tüm süreciniz için veritabanı dosyası başına tek bir SQLiteOpenHelper veya RoomDatabase örneği kullanın (ör. tekil yaparak). Her örneğin kendi bağlantı havuzu, sayfa önbellekleri ve ifade önbellekleri vardır. Aynı dosyayı açan iki örnek bunları paylaşamaz. Bir karşılaştırmada, aynı dosyayı açan iki yardımcıyı birleştirerek SQLite sayfa önbelleği belleğini neredeyse yarı yarıya, p95 sorgu gecikmesini ise yarıdan fazla azalttık.
Yinelenen örnekleri bulmak için birden fazla Connection pool for başlığının altında aynı veritabanı yolunu içeren dumpsys dbinfo öğesini kontrol edin.
Şema değişikliklerini açık geri çağırmanın dışında tutma
SQLiteOpenHelper sınıfının onOpen geri çağırma işlevi, veritabanı her açıldığında çalışır. RoomDatabase.Callback sınıfının onOpen geri çağırma işlevi de aynı şekilde çalışır. CREATE TABLE IF NOT EXISTS ve CREATE INDEX IF NOT EXISTS gibi ifadeleri onCreate ve onUpgrade içine veya Room taşıma işlemlerine yerleştirin. onOpen içinde setVersion çağrısı yapmayın. SQLiteOpenHelper, onCreate ve onUpgrade sonrasında sürümü zaten ayarlıyor. Şema zaten mevcut olsa bile bu ifadeler her açılışta çalışır ve setVersion, veritabanı dosyasına yazar.
Hazırlanan ifade önbelleğini boyutlandırma
Android, her bağlantı için derlenmiş ifadeleri önbelleğe alır. Önbellek varsayılan olarak 25 ifade tutar ve setMaxSqlCacheSize ile bu sayıyı MAX_SQL_CACHE_SIZE (100) değerine yükseltebilirsiniz. Bir veritabanı, etkin yollarında 25'ten fazla farklı ifade çalıştırırsa önbellek bozulur ve SQLite aynı ifadeleri tekrar tekrar derler.
Öncelikle sorgularınızı parametreleştirin. Her dize yeni bir ifade olduğundan, dize birleştirme ile oluşturulan SQL'e hiçbir önbellek boyutu yardımcı olamaz. Ardından, dumpsys meminfo içinde POOL STATS altındaki ifade önbelleği isabetlerini ve eksiklerini kontrol edin. Örneğin, bu bölümün örnek çıkışındaki phenotype.db veritabanında 147.921 isabet ve 89.616 isabetsizlik vardır. Bu da %38'lik bir isabetsizlik oranıdır. Bunun gibi bir veritabanında önbellek boyutunu artırmak, kaçırmaların neredeyse tamamını ortadan kaldırabilir. Önbelleğe alınan her ifade birkaç kilobayt bellek kullanır.
Önbellek boyutunu artırmak için SQLiteOpenHelper alt sınıfınızda onConfigure değerini geçersiz kılın:
override fun onConfigure(db: SQLiteDatabase) {
db.setMaxSqlCacheSize(SQLiteDatabase.MAX_SQL_CACHE_SIZE)
}
Room kullanıyorsanız RoomDatabase.Callback öğesinin onOpen geri çağırma işlevinde SupportSQLiteDatabase üzerinde aynı yöntemi çağırın.
Boştaki veritabanlarını kapatma, ancak meşgul olanları açık tutma
Açık bir veritabanı, boşta olduğunda bile bağlantılarını, önbelleklerini ve dosya tanımlayıcılarını korur. Bir karşılaştırmada, WAL modunda açık kalan boş bir Room veritabanı, yaklaşık 678 KB SQLite sayfa önbelleği belleği ve 5 dosya tanımlayıcı tuttu. Veritabanını yalnızca ara sıra (ör. günlük senkronizasyon sırasında) kullanıyorsanız işiniz bittiğinde kapatın.
Boştaki veritabanlarını güvenli bir şekilde kapatmak için aşağıdaki yönergeleri uygulayın:
- Room, deneysel bir API olan
setAutoCloseTimeoutile boşta olan bir veritabanını sizin için kapatabilir. Room, bir sonraki erişimde veritabanını yeniden açar veonOpengeri çağırma işlevinizi tekrar çalıştırır. Bu nedenle, bağlantı yapılandırmalarını buraya ekleyin. SQLiteOpenHelperveSQLiteDatabase.OpenParams.Builder'ninsetIdleConnectionTimeoutyöntemlerini kullanmaktan kaçının.PRAGMAifadeleriyle uyguladığınız ayarları geri yüklemeden bağlantıları kapattıkları için API düzeyi 29'da kullanımdan kaldırılmıştır.- Birkaç saniyede bir sorguladığınız bir veritabanını kapatmayın. Sorgularınız arasındaki boşluktan daha kısa bir zaman aşımı, veritabanının tekrar tekrar kapanıp açılmasına neden olur. Bu da her seferinde sayfa önbelleğini ve ifade önbelleğini silerek bir sonraki sorguyu yavaşlatır.
SQLite belleğini hesaba katma
SQLite bellek sorunlarını tespit etmek zor olabilir. SQLite sayfa önbelleği ve ifade önbelleği, yerel yığın ayırmalardır. Ancak Android, SQLite'ın bellekle eşlenen G/Ç'sini varsayılan olarak etkinleştirmez. Bu nedenle SQLite, veritabanı dosyalarını normal dosya okuma işlemleriyle okur ve çekirdeğin önbelleğe aldığı dosya içerikleri, eşlenmemiş dosya sayfalarıdır. Bu kullanıcılar, uygulamanızın dumpsys meminfo için PSS'sine dahil edilmez. cgroup v2 bellek hesaplaması kullanan cihazlarda çekirdek, bu sayfaları uygulamanızın bellek cgroup'una eklemeye devam eder. Bu gizli ayak izini azaltmak için dizinleri kullanarak daha az sayfa okuyun ve yalnızca ihtiyacınız olan satır ve sütunları okuyun. İhtiyacınız olmayan veritabanlarını kapatmak, SQLite önbelleklerini de boşaltır.
Verimli tablo şemaları tanımlama
Performansı optimize etmek ve veri tüketimini en aza indirmek için verimli bir tablo şeması tanımlayın. SQLite, verimli sorgu planları ve veriler oluşturarak verilerin daha hızlı alınmasını sağlar. Bu bölümde, tablo şemaları oluşturmayla ilgili en iyi uygulamalar açıklanmaktadır.
INTEGER PRIMARY KEY kategorisini düşünün
Bu örnekte, bir tabloyu aşağıdaki gibi tanımlayın ve doldurun:
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');
Tablo çıkışı aşağıdaki gibidir:
| rowid | id | ad | şehir |
|---|---|---|---|
| 1 | 456 | John Lennon | Liverpool, İngiltere |
| 2 | 123 | Michael Jackson | Gary, IN |
| 3 | 789 | Dolly Parton | Sevier County, TN |
rowid sütunu, ekleme sırasını koruyan bir dizindir. rowid ile filtreleme yapan sorgular hızlı bir B-tree araması olarak uygulanır ancak id ile filtreleme yapan sorgular yavaş bir tablo taramasıdır.
id ile arama yapmayı planlıyorsanız depolama alanında daha az veri ve genel olarak daha hızlı bir veritabanı için rowid sütununu depolamaktan kaçınabilirsiniz:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
city TEXT
);
Tablonuz artık aşağıdaki gibi görünür:
| id | ad | şehir |
|---|---|---|
| 123 | Michael Jackson | Gary, IN |
| 456 | John Lennon | Liverpool, İngiltere |
| 789 | Dolly Parton | Sevier County, TN |
rowid sütununu depolamanız gerekmediğinden id sorguları hızlıdır. Tablonun artık kampanya siparişi yerine id ölçütüne göre sıralandığını görebilirsiniz.
Dizinlerle sorguları hızlandırma
SQLite, sorguları hızlandırmak için dizinleri kullanır. Bir sütunu filtrelerken (WHERE), sıralarken (ORDER BY) veya toplarken (GROUP BY) tabloda sütun için bir dizin varsa sorgu hızlandırılır.
Önceki örnekte, city ölçütüne göre filtreleme yapmak için tablonun tamamının taranması gerekir:
SELECT id, name
WHERE city = 'London, England';
Çok sayıda şehir sorgusu içeren bir uygulama için bu sorguları bir dizinle hızlandırabilirsiniz:
CREATE INDEX city_index ON Customers(city);
Dizin, dizin sütununa göre sıralanan ve rowid ile eşlenen ek bir tablo olarak uygulanır:
| şehir | rowid |
|---|---|
| Gary, IN | 2 |
| Liverpool, İngiltere | 1 |
| Sevier County, TN | 3 |
city sütununun depolama maliyetinin artık iki katına çıktığını unutmayın. Bunun nedeni, sütunun hem orijinal tabloda hem de dizinde bulunmasıdır. Dizini kullandığınız için ek depolama alanının maliyeti, daha hızlı sorguların avantajına değer.
Ancak sorgu performansı artışı olmadan depolama maliyeti ödememek için kullanmadığınız bir dizini saklamayın.
Çok sütunlu dizinler oluşturma
Sorgularınız birden fazla sütunu birleştiriyorsa sorguyu tamamen hızlandırmak için çok sütunlu dizinler oluşturabilirsiniz. Dış sütunda bir dizin kullanıp iç aramanın doğrusal tarama olarak yapılmasını da sağlayabilirsiniz.
Örneğin, aşağıdaki sorguyu ele alalım:
SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name
Sorguda belirtilen sırayla çok sütunlu bir dizin kullanarak sorguyu hızlandırabilirsiniz:
CREATE INDEX city_name_index ON Customers(city, name);
Ancak yalnızca city üzerinde bir dizininiz varsa dış sıralama yine hızlandırılır, iç sıralama ise doğrusal tarama gerektirir.
Bu, önek sorgularında da geçerlidir. Örneğin, bir dizin
ON Customers (city, name), city'ye göre filtreleme, sıralama ve gruplandırmayı da hızlandırır.
Çünkü çok sütunlu bir dizinin dizin tablosu, verilen dizinlere göre verilen sırada sıralanır.
WITHOUT ROWID kategorisini düşünün
SQLite, varsayılan olarak tablonuz için bir rowid sütunu oluşturur. Burada rowid, örtük bir INTEGER PRIMARY KEY AUTOINCREMENT değeridir. Zaten INTEGER PRIMARY KEY olan bir sütununuz varsa bu sütun, rowid için bir takma ad olur.
INTEGER dışında bir birincil anahtarı veya sütun bileşimi olan tablolar için WITHOUT ROWID kullanabilirsiniz.
Küçük verileri BLOB, büyük verileri dosya olarak depolayın
Bir satırla büyük verileri (ör. bir resmin küçük resmi veya bir kişinin fotoğrafı) ilişkilendirmek istiyorsanız verileri bir BLOB sütununda veya bir dosyada depolayabilir, ardından yolu sütunda depolayabilirsiniz.
Dosyalar genellikle 4 KB'lık artışlarla yuvarlanır. Yuvarlama hatasının önemli olduğu çok küçük dosyalar için bunları veritabanında BLOB olarak depolamak daha verimlidir. SQLite, dosya sistemi çağrılarını en aza indirir ve bazı durumlarda temel dosya sisteminden daha hızlıdır.
Sorgu performansını iyileştirme
Yanıt sürelerini en aza indirip işleme verimliliğini en üst düzeye çıkararak SQLite'ta sorgu performansını iyileştirmek için aşağıdaki en iyi uygulamaları izleyin.
Yalnızca ihtiyacınız olan satırları okuma
Filtreler, tarih aralığı, konum veya ad gibi belirli ölçütleri belirterek sonuçlarınızı daraltmanıza olanak tanır. Sınırlar, gördüğünüz sonuç sayısını kontrol etmenizi sağlar:
db.rawQuery("""
SELECT name
FROM Customers
LIMIT 10;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Process cursor data
}
}
Yalnızca ihtiyacınız olan sütunları okuma
Sorgularınızı yavaşlatabileceği ve kaynakları boşa harcayabileceği için gereksiz sütunları seçmekten kaçının. Bunun yerine, yalnızca kullanılan sütunları seçin.
Aşağıdaki örnekte id, name ve phone'yi seçersiniz:
// 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
}
}
Ancak yalnızca name sütununa ihtiyacınız vardır:
db.rawQuery("""
SELECT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
val name = cursor.getString(0)
// Further processing
}
}
Sorguları parametreleştirme
Sorgu dizeniz, yalnızca çalışma zamanında bilinen bir parametre içerebilir. Örneğin:
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
}
}
}
Önceki kodda her sorgu farklı bir dize oluşturur ve bu nedenle ifade önbelleğinden yararlanmaz. Her çağrının yürütülebilmesi için SQLite'ın çağrıyı derlemesi gerekir. Bunun yerine, id bağımsız değişkenini bir parametre ile değiştirebilir ve değeri selectionArgs ile bağlayabilirsiniz:
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
}
}
}
Artık sorgu bir kez derlenip önbelleğe alınabilir. Derlenen sorgu, getNameById(long) öğesinin farklı çağrıları arasında yeniden kullanılır.
Kodda değil, SQL'de yineleme yapın
Tek tek sonuçlar döndürmek için SQL sorguları üzerinde yineleme yapan programatik bir döngü yerine, hedeflenen tüm sonuçları döndüren tek bir sorgu kullanın. Programatik döngü, tek bir SQL sorgusundan yaklaşık 1.000 kat daha yavaştır.
Benzersiz değerler için DISTINCT kullanın
DISTINCT anahtar kelimesini kullanmak, işlenmesi gereken veri miktarını azaltarak sorgularınızın performansını artırabilir. Örneğin, bir sütundaki yalnızca benzersiz değerleri döndürmek istiyorsanız DISTINCT işlevini kullanın:
db.rawQuery("""
SELECT DISTINCT name
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
// Only iterate over distinct names in Kotlin
// Process distinct name
}
}
Mümkün olduğunda topla işlevlerini kullanın
Satır verileri olmadan toplu sonuçlar için topla işlevlerini kullanın. Örneğin, aşağıdaki kod en az bir eşleşen satır olup olmadığını kontrol eder:
// 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
}
Yalnızca ilk satırı getirmek için EXISTS() işlevini kullanarak eşleşen bir satır yoksa 0, bir veya daha fazla satır eşleşiyorsa 1 değerini döndürebilirsiniz:
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
}
}
Uygulama kodunuzda SQLite toplama işlevlerini kullanma:
COUNT: Bir sütunda kaç satır olduğunu sayar.SUM: Bir sütundaki tüm sayısal değerleri toplar.MINveyaMAX: En düşük veya en yüksek değeri belirler. Sayısal sütunlar,DATEtürleri ve metin türleri için kullanılabilir.AVG: Ortalama sayısal değeri bulur.GROUP_CONCAT: Dizeleri isteğe bağlı bir ayırıcıyla birleştirir.
Cursor.getCount() yerine COUNT() kullanın
Aşağıdaki örnekte, Cursor.getCount() işlevi veritabanındaki tüm satırları okur ve tüm satır değerlerini döndürür:
// 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
}
Ancak COUNT() kullanıldığında veritabanı yalnızca sayıyı döndürür:
db.rawQuery("""
SELECT COUNT(*)
FROM Customers;
""".trimIndent(),
null
).use { cursor ->
cursor.moveToFirst()
val count = cursor.getInt(0)
// Use count
}
Kod yerine sorguları iç içe yerleştirme
SQL, birleştirilebilir ve alt sorguları, birleştirmeleri ve yabancı anahtar kısıtlamalarını destekler. Bir sorgunun sonucunu, uygulama kodunu kullanmadan başka bir sorguda kullanabilirsiniz. Bu sayede SQLite'tan veri kopyalama ihtiyacı azalır ve veritabanı motoru sorgunuzu optimize edebilir.
Aşağıdaki örnekte, en çok müşterinin hangi şehirde olduğunu bulmak için bir sorgu çalıştırabilir, ardından sonucu başka bir sorguda kullanarak o şehirdeki tüm müşterileri bulabilirsiniz:
// 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
}
}
}
}
Sonucu önceki örneğin yarısı kadar sürede almak için iç içe yerleştirilmiş ifadeler içeren tek bir SQL sorgusu kullanın:
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
}
}
SQL'de benzersizliği kontrol etme
Bir satırın, tablodaki belirli bir sütun değeri benzersiz olmadığı sürece eklenmemesi gerekiyorsa bu benzersizliği sütun kısıtlaması olarak zorunlu kılmak daha verimli olabilir.
Aşağıdaki örnekte, eklenecek satırı doğrulamak için bir sorgu, ekleme işlemini gerçekleştirmek için başka bir sorgu çalıştırılır:
// 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
)
)
Benzersizlik kısıtlamasını Kotlin yerine SQL'de tabloyu tanımlarken kontrol edebilirsiniz:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
username TEXT UNIQUE
);
SQLite, aşağıdakilerle aynı işlemi yapar:
CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);
Şimdi bir satır ekleyip SQLite'ın kısıtlamayı kontrol etmesini sağlayabilirsiniz:
try {
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
arrayOf(customer.id.toString(), customer.name, customer.username)
)
} catch(e: SQLiteConstraintException) {
throw AddCustomerException(customer, e)
}
SQLite, birden çok sütun içeren benzersiz dizinleri destekler:
CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);
SQLite, kısıtlamaları Kotlin kodundan daha hızlı ve daha az ek yükle doğrular. Uygulama kodu yerine SQLite kullanmak en iyi uygulamadır.
Tek bir işlemde birden fazla ekleme işlemini toplu olarak gerçekleştirme
İşlem, birden fazla işlemi yürütür. Bu sayede hem verimlilik hem de doğruluk artar. Veri tutarlılığını artırmak ve performansı hızlandırmak için ekleme işlemlerini toplu olarak yapabilirsiniz:
db.beginTransaction()
try {
customers.forEach { customer ->
db.execSQL(
"INSERT INTO Customers VALUES (?, ?, ?)",
arrayOf(customer.id.toString(), customer.name, "customerValue")
)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
Açık bir işlem dışında her yazma işlemi kendi örtük işleminde çalışır ve eşzamanlı modda FULL içinde fsync dahil olmak üzere bir kaydetme işleminin tüm maliyetini öder. Toplu işleme, bu maliyeti bir kez öder. Bu da bir yazma grubunu bir kat veya daha fazla hızlandırabilir.
Sorun giderme araçlarını kullanma
SQLite, performansı ölçmeye yardımcı olmak için aşağıdaki sorun giderme araçlarını sağlar.
SQLite'ın etkileşimli istemini kullanma
Sorgu çalıştırmak ve öğrenmek için makinenizde SQLite'ı çalıştırın.
Farklı Android platformu sürümleri, SQLite'ın farklı düzeltmelerini kullanır. Android destekli bir cihazdaki motorun aynısını kullanmak için adb shell komutunu kullanın ve hedef cihazınızda sqlite3 komutunu çalıştırın.
SQLite'tan sorguları zamanlamasını isteyebilirsiniz:
sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...
EXPLAIN QUERY PLAN
SQLite'tan, EXPLAIN QUERY PLAN kullanarak bir sorguyu nasıl yanıtlamayı planladığını açıklamasını isteyebilirsiniz:
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers
Önceki örnekte, Paris'teki tüm müşterileri bulmak için dizin olmadan tam tablo taraması yapılması gerekir. Buna doğrusal karmaşıklık denir. SQLite'ın tüm satırları okuması ve yalnızca Paris'teki müşterilerle eşleşen satırları tutması gerekir. Bu sorunu düzeltmek için bir dizin ekleyebilirsiniz:
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=?
Etkileşimli kabuğu kullanıyorsanız SQLite'tan sorgu planlarını her zaman açıklamasını isteyebilirsiniz:
sqlite> .eqp on
Daha fazla bilgi için Sorgu Planlama bölümüne bakın.
SQLite Analyzer
SQLite, performans sorunlarını gidermek için kullanılabilecek ek bilgileri dökümlemek üzere sqlite3_analyzer komut satırı arayüzünü (KSA) sunar. Yüklemek için SQLite indirme sayfasını ziyaret edin.
Bir veritabanı dosyasını hedef cihazdan iş istasyonunuza indirip analiz etmek için adb pull aracını kullanabilirsiniz:
adb pull /data/data/<app_package_name>/databases/<db_name>.db
SQLite Browser
SQLite indirme sayfasından SQLite Browser GUI aracını da yükleyebilirsiniz.
Android günlüğü
Android, SQLite sorgularını zamanlar ve sizin için günlüğe kaydeder:
# Enable query time logging
$ adb shell setprop log.tag.SQLiteTime VERBOSE
# Disable query time logging
$ adb shell setprop log.tag.SQLiteTime ERROR
Android 13 ve sonraki sürümlerde yalnızca yavaş işlemleri de günlüğe kaydedebilirsiniz. Milisaniye cinsinden bir eşik belirleyin, ardından günlük etiketini okuması için uygulamanızı yeniden başlatın:
$ adb shell setprop log.tag.SQLiteSlowQueries VERBOSE
$ adb shell setprop db.log.slow_query_threshold 100
$ adb shell am force-stop <package-name>
Yavaş işlemler, logcat'te SQLiteConnection etiketi altında süreleri, SQL ve veritabanı yollarıyla birlikte gösterilir. Tek bir uygulamanın eşiğini ayarlamak için db.log.slow_query_threshold.<uid> komutunu kullanın. Burada <uid>, uygulamanın UID'sidir.
Perfetto izleme
Perfetto'yu yapılandırırken, SQLite işlemlerini izlemeye dahil etmek için database atrace kategorisini ekleyin. SQL çalıştırırken her bir iş parçacığının ne yaptığını görmek için zamanlayıcı etkinliklerini de ekleyin:
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "sched/sched_blocked_reason"
atrace_categories: "database"
}
}
}
Android, her veritabanı işlemini, işlemin ve SQL'inin adını taşıyan bir dilim olarak izler (ör. executeForCursorWindow SELECT * FROM message WHERE body LIKE ?). SQL, ? yer tutucularını korur ve 256 karaktere kısaltılır. Bağlama bağımsız değişkenleri dahil edilmez. İşlemler arasında open, close, prepare, execute ve executeFor varyantları yer alır. prepare dilimi, ifade önbelleğinin kaçırıldığı anlamına gelmez.
Kanal düzeni, Android sürümüne bağlıdır:
- Android 17 ve sonraki sürümlerde her bağlantının, uygulama işleminde
SQLiteConnection-ve ardından bağlantı numarasıyla adlandırılan eşzamansız bir kanalı vardır. İşlemlerTRANSACTION-EXCLUSIVE,TRANSACTION-IMMEDIATEveyaTRANSACTION-DEFERREDdilimleri olarak görünür ve içlerinde çalıştırılan ifadeler altlarında iç içe yerleştirilir. Bir işlemi çalıştıran iş parçacığı, her işlem ve işlem dışındaki her ifade için iş parçacığı izindeSTART-veEND-anlık etkinlikleri de alır. Bu etkinliklerin adları, bağlantı kanalı adıyla biter (ör.-SQLiteConnection-0). - Android 16 ve önceki sürümlerde her farklı SQL ifadesinin uygulama sürecinde kendi eş zamansız izi vardır ve işlemler izlenmez.
Bağlantı numaraları her veritabanı için 0'dan başlar. Bu nedenle, bir işlemdeki tüm veritabanlarının birincil bağlantıları SQLiteConnection-0 izini paylaşır. Uygulamanız aynı anda birden fazla veritabanı kullanıyorsa iş parçacığı izlerindeki START- ve END- etkinlikleriyle süreleri ölçün.
Şekil 1. Android 17'de ana iş parçacığında tam tablo taraması.
Bu dilimleri Perfetto kullanıcı arayüzünde PerfettoSQL ile veya trace_processor ile analiz edebilirsiniz. Aşağıdaki sorgu, her işlemde en sık kullanılan SQLite işlemlerini bulur. Bu sorgu tüm Android sürümlerinde çalışır:
INCLUDE PERFETTO MODULE slices.with_context;
SELECT process_name, name AS operation, COUNT(*) AS count
FROM process_slice
WHERE name GLOB 'open *' OR name GLOB 'close *'
OR name GLOB 'prepare *' OR name GLOB 'execute*'
OR name GLOB 'TRANSACTION-*'
GROUP BY process_name, name
ORDER BY count DESC
LIMIT 10;
Aşağıdaki sorguda, Android 17 ve sonraki sürümlerde ana iş parçacığında 1 ms'den uzun süren SQLite işlemleri listelenmektedir:
INCLUDE PERFETTO MODULE slices.with_context;
WITH marks AS (
SELECT process_name, utid, is_main_thread, ts, name,
LEAD(ts) OVER (PARTITION BY utid ORDER BY ts) AS end_ts,
LEAD(name) OVER (PARTITION BY utid ORDER BY ts) AS end_name
FROM thread_slice
WHERE name GLOB 'START-*' OR name GLOB 'END-*'
)
SELECT process_name, ts, ROUND((end_ts - ts) / 1e6, 1) AS dur_ms,
substr(name, 7) AS operation
FROM marks
WHERE name GLOB 'START-*'
AND end_name = 'END-' || substr(name, 7)
AND is_main_thread
AND end_ts - ts > 1e6
ORDER BY dur_ms DESC
LIMIT 10;
Şekil 2. Ana iş parçacığında 1 ms'den uzun süren SQLite işlemleri.
Aşağıdaki sorgu, Android 17 ve sonraki sürümlerde açık bir işlemin dışında yazma işlemi yapan ve her yazma işleminin kendi kaydı için ödeme yaptığı ileti dizilerini bulur:
INCLUDE PERFETTO MODULE slices.with_context;
WITH ops AS (
SELECT process_name, thread_name, utid, name,
upper(ltrim(substr(name, instr(name, ' ') + 1))) AS sql
FROM thread_slice
WHERE name GLOB 'START-*'
)
SELECT process_name, thread_name,
SUM(name GLOB 'START-TRANSACTION-*') AS transactions,
SUM(name GLOB 'START-execute*' AND (sql GLOB 'INSERT *'
OR sql GLOB 'UPDATE *' OR sql GLOB 'DELETE *' OR sql GLOB 'REPLACE *'))
AS autocommit_writes
FROM ops
GROUP BY utid
HAVING autocommit_writes > 0
ORDER BY autocommit_writes DESC
LIMIT 10;
dumpsys meminfo
adb shell dumpsys meminfo <package-name>, uygulamanın bellek kullanımıyla ilgili istatistikleri yazdırır. DATABASES bölümünde SQLite ile ilgili ayrıntılar da yer alır. Örneğin, aşağıdaki çıkış bir geliştiricinin cihazında adb shell dumpsys meminfo com.google.android.gms.persistent komutunun çalıştırılmasıyla elde edilmiştir:
DATABASES
pgsz dbsz Lookaside(b) cache hits cache misses cache size Dbname
PER CONNECTION STATS
4 52 45 8 41 6 /data/user/10/com.google.android.gms/databases/gaia-discovery
4 8 0 0 0 (attached) temp
4 52 56 5 23 6 /data/user/10/com.google.android.gms/databases/gaia-discovery (1)
4 252 95 233 124 12 /data/user_de/10/com.google.android.gms/databases/phenotype.db
4 8 0 0 0 (attached) temp
4 252 17 0 17 1 /data/user_de/10/com.google.android.gms/databases/phenotype.db (1)
4 9280 105 103169 69805 25 /data/user/10/com.google.android.gms/databases/phenotype.db
4 20 0 0 0 (attached) temp
4 9280 108 13877 6394 25 /data/user/10/com.google.android.gms/databases/phenotype.db (2)
4 8 0 0 0 (attached) temp
4 9280 105 12548 5519 25 /data/user/10/com.google.android.gms/databases/phenotype.db (3)
4 8 0 0 0 (attached) temp
4 9280 107 18328 7886 25 /data/user/10/com.google.android.gms/databases/phenotype.db (1)
4 8 0 0 0 (attached) temp
4 36 51 156 29 5 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
4 36 97 47 27 10 /data/user/10/com.google.android.gms/databases/context_feature_default.db
4 36 56 3 16 4 /data/user/10/com.google.android.gms/databases/context_feature_default.db (2)
4 300 40 2111 24 5 /data/user/10/com.google.android.gms/databases/gservices.db
4 300 39 3 17 4 /data/user/10/com.google.android.gms/databases/gservices.db (1)
4 20 17 0 14 1 /data/user/10/com.google.android.gms/databases/gms.notifications.db
4 20 33 1 15 2 /data/user/10/com.google.android.gms/databases/gms.notifications.db (1)
4 120 40 143 163 4 /data/user/10/com.google.android.gms/databases/android_pay
4 120 123 86 32 19 /data/user/10/com.google.android.gms/databases/android_pay (1)
4 28 33 4 17 3 /data/user/10/com.google.android.gms/databases/googlesettings.db
POOL STATS
cache hits cache misses cache size Dbname
13 68 81 /data/user/10/com.google.android.gms/databases/gaia-discovery
233 145 378 /data/user_de/10/com.google.android.gms/databases/phenotype.db
147921 89616 237537 /data/user/10/com.google.android.gms/databases/phenotype.db
156 30 186 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
50 57 107 /data/user/10/com.google.android.gms/databases/context_feature_default.db
2114 43 2157 /data/user/10/com.google.android.gms/databases/gservices.db
1 31 32 /data/user/10/com.google.android.gms/databases/gms.notifications.db
229 197 426 /data/user/10/com.google.android.gms/databases/android_pay
4 18 22 /data/user/10/com.google.android.gms/databases/googlesettings.db
Android 14 ve sonraki sürümler, örnekte gösterilen PER CONNECTION STATS ve POOL STATS tablolarını yazdırır. Android 13 ve önceki sürümler yalnızca bağlantı başına tabloyu yazdırır. Önbellek istatistikleri, tek bir cache sütununda hits/misses/size olarak birleştirilir.
PER CONNECTION STATS bölümünde şunları bulabilirsiniz:
pgsz: KB cinsinden bir veritabanı sayfasının boyutu.dbsz: KB cinsinden tüm veritabanı dosyasının boyutu.Lookaside(b): Bağlantının kullandığı lookaside bellek yuvalarının sayısı. Başlıktaki(b)ifadesine rağmen bu, bayt sayısını değil, yuva sayısını gösterir.cache hits,cache missesvecache size: Bağlantının hazırlanmış ifade önbelleğiyle ilgili istatistikler. Bu değerler, veritabanı sayfalarını değil, derlenmiş SQL ifadelerini sayar.Dbname: Veritabanı dosyasının yolu. Aynı veritabanına yapılan ek bağlantıların sonuna parantez içinde bir sayı eklenir (ör.(1)). WAL ile bir veritabanının birden fazla bağlantısı olabilir. Böylece okuma işlemleri paralel olarak çalıştırılabilir.
POOL STATS bölümünde, her bağlantı havuzu için hazırlanmış ifade önbelleği istatistiklerini bulabilirsiniz:
cache hits: SQLite, SQL ifadelerini derlerken biraz çaba ve bellek tasarrufu sağlamak için hazırlanmış ifadeleri önbelleğe alır ve sorgu çalıştırırken bunları yeniden kullanmaya çalışır. Bu, talimat önbelleği isabetlerinin sayısıdır.cache misses: Önbellekte bulunamayan ifade sayısı.cache size: Android 16 QPR2 ve sonraki sürümlerde, önbellekteki hazırlanmış ifadelerin sayısı, havuzun boşta olan bağlantıları genelinde toplanır. Android 14'ten Android 16 QPR1'e kadar bu değer, isabetlerin ve ıskalamaların toplamıdır ve önbellek boyutunu temsil etmez.
Neleri düzelteceğinize karar vermek için bu istatistikleri kullanın:
- Yüksek bir isabet etmeme oranı, SQLite'ın aynı ifadeleri tekrar tekrar derlediği anlamına gelir.
Öncelikle dize birleştirme ile oluşturulmuş SQL'i bulun ve bunu parametreleştirin. Sorgularınız zaten parametreleştirilmişse ve bağlantı başına
cache size25 sınırındaysa önbellek boyutunu artırın. (1)stili bir son ek olmadan birden fazla kez görünen veritabanı yolu, uygulamanızın aynı dosyayı iki kez açtığı anlamına gelir. Her veritabanını bir kez açın.- Uygulamanızın nadiren kullandığı bir veritabanı, açık olduğu sürece bellek kullanmaya devam eder. İşiniz bittiğinde kapatın.
dumpsys dbinfo
adb shell dumpsys dbinfo <package-name>, uygulamanızdaki her bağlantı havuzunun durumunu yazdırır. Her bağlantının hazırlanmış ifade önbelleğindeki ifadeleri de listelemek için -v ekleyin. Aşağıdaki örnek çıkış, Android 17'den alınmıştır. Yollar ve uzun satırlar kısaltılmıştır:
** Database info for pid 2705 [com.example.app] **
Connection pool for /data/user/0/com.example.app/databases/settings.db:
Open: true
Max connections: 4
Total execution time (ms): 4963
Total statements executed: 231239
Average time per statement (ms): 0
Configuration: openFlags=805306368, ..., journalMode=WAL, syncMode=NORMAL
IsReadOnlyDatabase: false
Available primary connection:
<none>
Available non-primary connections:
<none>
Acquired connections:
<none>
Connection waiters:
<none>
Connection pool for /data/user/0/com.example.app/databases/events.db:
Open: true
Max connections: 1
Total execution time (ms): 842
Total statements executed: 840
Average time per statement (ms): 1
Configuration: openFlags=268435456, ..., journalMode=TRUNCATE, syncMode=FULL
IsReadOnlyDatabase: false
Available primary connection:
Connection #0:
isPrimaryConnection: true
onlyAllowReadOnlyOperations: false
totalLongOperations: 0
Most recently executed operations:
0: [10-26 04:22:32.133] executeForChangedRowCount took 0ms -
succeeded, sql="DELETE FROM params WHERE app_id=?", path=...
1: [10-26 04:22:32.133] prepare took 0ms - succeeded,
sql="DELETE FROM params WHERE app_id=?", path=...
...
Aşağıdakileri arayın:
Configuration: Havuzun günlük modu ve eşzamanlı modu. Android 13 ve sonraki sürümlerde bu satır, platform varsayılanları da dahil olmak üzere çerçevenin uyguladığı modları gösterir. Örnekteevents.db,FULLeşzamanlı moduyla geri alma günlüğünü kullanır. Bu nedenle, her kaydetme işlemi tam senkronizasyon için bekler. Bu satır, kendiniz çalıştırdığınızPRAGMAifadelerini yansıtmaz.Max connections: Yazılabilir bir veritabanı için 1 değeri, çerçevenin veritabanını WAL modunda kabul etmediği anlamına gelir. Bu nedenle, okuma ve yazma işlemleri tek bir bağlantıyı paylaşır.Total execution time,Total statements executedveAverage time per statement: Android 13 ve sonraki sürümlerde kullanılabilir. En yoğun veritabanlarınızı bulmak için bu metrikleri kullanabilirsiniz.Most recently executed operations: Her bağlantıdaki son 20 işlem, süreleri ve SQL'leri. Aynı sorgunun bir döngüde çalıştırılması veya bir işlem dışında arka arkaya birçok yazma işlemi yapılması gibi kalıplara dikkat edin. Android 15 QPR1 ve sonraki sürümlerde bu liste, açık işlemleri de içerir.totalLongOperationsveOperations exceeding 2000ms: Android 15 QPR1 ve sonraki sürümlerde kullanılabilir. Bu grafikler, 2 saniyeden uzun süren işlem ve transferlerin sayısını gösterir ve son 10 tanesini listeler.Acquired connectionsveConnection waiters: Kullanımda olan bağlantılar ve bağlantı bekleyen ileti dizileri. Bekleyenler, diğer yazma işlemlerini engelleyen uzun bir yazma işlemi gibi çekişmeleri gösterir.Prepared statement cache:-vile yazdırıldı. Yalnızca değişmez değerleri farklı olan birçok giriş, dize birleştirme ile oluşturulmuş SQL'e işaret eder.- Birden fazla
Connection pool forbaşlığı altında aynı yolun bulunması, uygulamanızın aynı dosyayı iki kez açtığı anlamına gelir.
Ek kaynaklar
İçerik görüntüleme
Sizin için önerilenler
- Not: JavaScript devre dışı bırakıldığında bağlantı metni gösterilir.
- Sürekli entegrasyonda karşılaştırmaları çalıştırma
- Donmuş kare
- Macrobenchmark olmadan Baseline Profiles oluşturma ve ölçme