Android SQLite-Datenbank: langsames Einfügen

91

Ich muss eine ziemlich große XML-Datei analysieren (die zwischen ungefähr hundert Kilobyte und mehreren hundert Kilobyte variiert), die ich verwende Xml#parse(String, ContentHandler). Ich teste dies derzeit mit einer 152-KB-Datei.

Während des Parsens füge ich die Daten auch in eine SQLite-Datenbank ein, indem ich Aufrufe ähnlich den folgenden verwende : getWritableDatabase().insert(TABLE_NAME, "_id", values). All dies zusammen dauert ungefähr 80 Sekunden für die 152-KB-Testdatei (was das Einfügen von ungefähr 200 Zeilen bedeutet).

Wenn ich alle Einfügeanweisungen auskommentiere (aber alles andere belasse, wie z. B. das Erstellen ContentValuesusw.), dauert dieselbe Datei nur 23 Sekunden.

Ist es normal, dass die Datenbankoperationen einen so großen Overhead haben? Kann ich etwas dagegen tun?

benvd
quelle

Antworten:

190

Sie sollten Batch-Einfügungen durchführen.

Pseudocode:

db.beginTransaction();
for (entry : listOfEntries) {
    db.insert(entry);
}
db.setTransactionSuccessful();
db.endTransaction();

Das hat die Geschwindigkeit der Einfügungen in meinen Apps extrem erhöht.

Update:
@Yuku lieferte einen sehr interessanten Blog-Beitrag: Android verwendet Inserthelper für schnellere Einfügungen in die SQLite-Datenbank

WarrenFaith
quelle
4
Das Einfügen aller ContentValues ​​auf diese Weise dauert nur ein oder zwei Sekunden. Vielen Dank!
Benvd
Ist es sicher, dass eine Transaktion mit langer Laufzeit geöffnet ist, die die gesamte Dauer des XML-Parsing-Vorgangs abdeckt und am Ende festgeschrieben wird? Oder sollte die Liste der Einfügungen lokal im XML-Parser zwischengespeichert werden und dann eine kurzlebige Transaktion geöffnet und festgeschrieben werden, sobald die Analyse abgeschlossen ist?
Graham Borland
2
Das Umschließen meiner 60 Beilagen mit einer Transaktion erhöhte die Leistung um das 10-fache. Das Umschließen mit einer Transaktion und das Verwenden einer vorbereiteten Anweisung (SQLiteStatement) hat sie um das 20-fache erhöht!
Stefs
2
benvd danke für den kommentar. Ich habe 20.000 Datensätze eingefügt, es hat ungefähr 8 Minuten gedauert, aber nach der Verwendung einer Transaktion dauert es nur 20 Sekunden :-)
Bear
2
Dieser Blogeintrag beschreibt eine weitere Optimierung mit dem fast versteckten InsertHelper outofwhatbox.com/blog/2010/12/…
Randy Sugianto 'Yuku'
68

Da der von Yuku und Brett erwähnte InsertHelper jetzt veraltet ist (API-Stufe 17), scheint die von Google empfohlene richtige Alternative die Verwendung von SQLiteStatement zu sein .

Ich habe die Datenbank-Einfügemethode wie folgt verwendet:

database.insert(table, null, values);

Nachdem ich auch einige schwerwiegende Leistungsprobleme hatte, beschleunigte der folgende Code meine 500 Einfügungen von 14,5 Sekunden auf nur 270 ms , erstaunlich!

So habe ich SQLiteStatement verwendet:

private void insertTestData() {
    String sql = "insert into producttable (name, description, price, stock_available) values (?, ?, ?, ?);";

    dbHandler.getWritableDatabase();
    database.beginTransaction();
    SQLiteStatement stmt = database.compileStatement(sql);

    for (int i = 0; i < NUMBER_OF_ROWS; i++) {
        //generate some values

        stmt.bindString(1, randomName);
        stmt.bindString(2, randomDescription);
        stmt.bindDouble(3, randomPrice);
        stmt.bindLong(4, randomNumber);

        long entryID = stmt.executeInsert();
        stmt.clearBindings();
    }

    database.setTransactionSuccessful();
    database.endTransaction();

    dbHandler.close();
}
qefzec
quelle
14
Ein Haken, den Sie hier vermeiden sollten: Der Index in bindString basiert auf 1 und nicht auf 0
Anzeigename
@qefzec Vielen Dank für diese Lösung. Dadurch wurde die Einfügezeit in meiner App von 78 Sekunden auf 4 Sekunden für 900 hinzugefügte Zeilen
gesenkt
1
Danke mein Herr. 20000 Datensätze mit jeweils 6 Datenfeldern, einschließlich VARCHAR (80) von 4 Minuten bis 8 Sekunden. Eigentlich sollte dies als beste Antwort markiert werden, IMHO.
TomeeNS
1
Toll! Unser Benchmark für 200 Testeinsätze gleichzeitig mit 15 Spalten pro Einsatz ergab je nach Gerät und internem / externem Speicher eine Verbesserung zwischen 4100% und 10400%. Vorherige Leistungen hätten unser Projekt zum Scheitern verurteilt, bevor es auf den Weg gebracht wurde.
Frank
von 15
Minuten
13

Das Kompilieren der SQL-Insert-Anweisung beschleunigt die Arbeit. Es kann auch mehr Aufwand erfordern, um alles zu stützen und eine mögliche Injektion zu verhindern, da jetzt alles auf Ihren Schultern liegt.

Ein weiterer Ansatz, der ebenfalls die Arbeit beschleunigen kann, ist die unterdokumentierte Klasse android.database.DatabaseUtils.InsertHelper. Mein Verständnis ist, dass es tatsächlich kompilierte Einfügeanweisungen umschließt. Der Wechsel von nicht kompilierten Transaktions-Inserts zu kompilierten Transaktions-Inserts war für meine großen (200K + Einträge), aber einfachen SQLite-Inserts etwa dreimal schneller (2 ms pro Insert bis 0,6 ms pro Insert).

Beispielcode:

SQLiteDatabse db = getWriteableDatabase();

//use the db you would normally use for db.insert, and the "table_name"
//is the same one you would use in db.insert()
InsertHelper iHelp = new InsertHelper(db, "table_name");

//Get the indices you need to bind data to
//Similar to Cursor.getColumnIndex("col_name");                 
int first_index = iHelp.getColumnIndex("first");
int last_index = iHelp.getColumnIndex("last");

try
{
   db.beginTransaction();
   for(int i=0 ; i<num_things ; ++i)
   {
       //need to tell the helper you are inserting (rather than replacing)
       iHelp.prepareForInsert();

       //do the equivalent of ContentValues.put("field","value") here
       iHelp.bind(first_index, thing_1);
       iHelp.bind(last_index, thing_2);

       //the db.insert() equilvalent
       iHelp.execute();
   }
   db.setTransactionSuccessful();
}
finally
{
    db.endTransaction();
}
db.close();
Brett
quelle
und wie man ContentValue in iHelp.bind hinzufügt (first_index, thing_1); ?
Vasil Valchev
3

Wenn die Tabelle einen Index enthält, sollten Sie ihn vor dem Einfügen der Datensätze löschen und nach dem Festschreiben Ihrer Datensätze wieder hinzufügen.

kaD'argo
quelle
1

Bei Verwendung eines ContentProviders:

@Override
public int bulkInsert(Uri uri, ContentValues[] bulkinsertvalues) {

    int QueryType = sUriMatcher.match(uri);
    int returnValue=0;
    SQLiteDatabase db = mOpenHelper.getWritableDatabase();

     switch (QueryType) {

         case SOME_URI_IM_LOOKING_FOR: //replace this with your real URI

            db.beginTransaction();

            for (int i = 0; i < bulkinsertvalues.length; i++) {
                //get an individual result from the array of ContentValues
                ContentValues values = bulkinsertvalues[i];
                //insert this record into the local SQLite database using a private function you create, "insertIndividualRecord" (replace with a better function name)
                insertIndividualRecord(uri, values);    
            }

            db.setTransactionSuccessful();
            db.endTransaction();                 

            break;  

         default:
             throw new IllegalArgumentException("Unknown URI " + uri);

     }    

    return returnValue;

}

Dann die private Funktion zum Ausführen der Einfügung (noch in Ihrem Inhaltsanbieter):

       private Uri insertIndividualRecord(Uri uri, ContentValues values){

            //see content provider documentation if this is confusing
            if (sUriMatcher.match(uri) != THE_CONSTANT_IM_LOOKING_FOR) {
                throw new IllegalArgumentException("Unknown URI " + uri);
            }

            //example validation if you have a field called "name" in your database
            if (values.containsKey(YOUR_CONSTANT_FOR_NAME) == false) {
                values.put(YOUR_CONSTANT_FOR_NAME, "");
            }

            //******add all your other validations

            //**********

           //time to insert records into your local SQLite database
           SQLiteDatabase db = mOpenHelper.getWritableDatabase();
           long rowId = db.insert(YOUR_TABLE_NAME, null, values);           

           if (rowId > 0) {
               Uri myUri = ContentUris.withAppendedId(MY_INSERT_URI, rowId);
               getContext().getContentResolver().notifyChange(myUri, null);

               return myUri;
           }


           throw new SQLException("Failed to insert row into " + uri);


    }
CircuitBreaker716
quelle