Base de données Android SQLite: insertion lente

91

J'ai besoin d'analyser un fichier XML assez volumineux (variant entre une centaine de kilo-octets et plusieurs centaines de kilo-octets), ce que j'utilise Xml#parse(String, ContentHandler). Je teste actuellement cela avec un fichier de 152 Ko.

Au cours de l' analyse syntaxique, je également insérer les données dans une base de données SQLite utilisant des appels similaires à ce qui suit: getWritableDatabase().insert(TABLE_NAME, "_id", values). Tout cela prend environ 80 secondes pour le fichier de test de 152 Ko (ce qui revient à insérer environ 200 lignes).

Lorsque je commente toutes les instructions d'insertion (mais que je laisse tout le reste, comme la création, ContentValuesetc.), le même fichier ne prend que 23 secondes.

Est-il normal que les opérations de base de données aient une telle surcharge? Puis-je faire quelque chose à ce sujet?

benvd
la source

Réponses:

190

Vous devriez faire des insertions par lots.

Pseudocode:

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

Cela a considérablement augmenté la vitesse des insertions dans mes applications.

Mise à jour:
@Yuku a fourni un article de blog très intéressant: Android utilise inserthelper pour des insertions plus rapides dans la base de données sqlite

WarrenFaith
la source
4
L'insertion de toutes les ContentValues ​​de cette manière ne prend qu'une seconde ou deux. Merci beaucoup!
benvd
Est-il sûr d'avoir une transaction de longue durée ouverte, couvrant toute la durée de l'opération d'analyse XML, puis de la valider à la fin? Ou la liste des insertions doit-elle être mise en cache localement dans l'analyseur XML et avoir une transaction de courte durée ouverte et validée une fois l'analyse terminée?
Graham Borland
2
envelopper mes 60 insertions avec une transaction a augmenté les performances 10x. l'enveloppant avec une transaction et l'utilisation d'une instruction préparée (SQLiteStatement) l'a augmenté de 20x!
stefs
2
merci benvd pour le commentaire. J'étais en train d'insérer 20k enregistrements, cela a pris environ 8 minutes, mais après avoir utilisé une transaction, cela ne prend que 20 secondes :-)
Ours
2
Cette entrée de blog discute d'une autre optimisation utilisant le InsertHelper presque caché outofwhatbox.com/blog/2010/12/ ...
Randy Sugianto 'Yuku'
68

Étant donné que InsertHelper mentionné par Yuku et Brett est désormais obsolète (niveau d'API 17), il semble que la bonne alternative recommandée par Google soit l'utilisation de SQLiteStatement .

J'ai utilisé la méthode d'insertion de base de données comme ceci:

database.insert(table, null, values);

Après avoir également rencontré de sérieux problèmes de performances, le code suivant a accéléré mes 500 insertions de 14,5 secondes à seulement 270 ms , incroyable!

Voici comment j'ai utilisé SQLiteStatement:

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
la source
14
Un hic à éviter ici: l'index dans bindString est basé sur 1 et non sur 0
Nom d'affichage
@qefzec Merci pour cette solution ..
Cela vient de faire passer
1
Merci Monsieur. 20000 enregistrements avec 6 champs de données chacun, dont VARCHAR (80) de 4 minutes à 8 secondes. En fait, cela devrait être marqué comme la meilleure réponse, à mon humble avis.
TomeeNS
1
Génial! Notre benchmark sur 200 inserts de test à la fois avec 15 colonnes par insert a généré entre 4100% et 10400% d'amélioration en fonction de l'appareil et de la mémoire interne / externe. Des performances antérieures auraient condamné notre projet avant qu'il ne démarre.
Frank
de 15min à 45s pour 13600 lignes
DoruChidean
13

La compilation de l'instruction d'insertion SQL permet d'accélérer les choses. Cela peut également nécessiter plus d'efforts pour tout consolider et empêcher une éventuelle injection, car tout est désormais sur vos épaules.

Une autre approche qui peut également accélérer les choses est la classe android.database.DatabaseUtils.InsertHelper sous-documentée. Je crois comprendre qu'il encapsule en fait les instructions d'insertion compilées. Passer des insertions transactées non compilées aux insertions transactées compilées représentait un gain de vitesse d'environ 3x (2 ms par insert à 0,6 ms par insert) pour mes insertions SQLite volumineuses (200K + entrées) mais simples.

Exemple de code:

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
la source
et comment ajouter ContentValue dans iHelp.bind (first_index, thing_1); ?
Vasil Valchev
3

Si la table contient un index, envisagez de le supprimer avant d'insérer les enregistrements, puis de le rajouter après avoir validé vos enregistrements.

kaD'argo
la source
1

Si vous utilisez un ContentProvider:

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

}

Ensuite, la fonction privée pour effectuer l'insertion (toujours à l'intérieur de votre fournisseur de contenu):

       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
la source