Base de datos SQLite de Android: inserción lenta

91

Necesito analizar un archivo XML bastante grande (que varía entre unos cien kilobytes y varios cientos de kilobytes), que estoy usando Xml#parse(String, ContentHandler). Actualmente estoy probando esto con un archivo de 152 KB.

Durante el análisis, también insertar los datos en una base de datos SQLite utilizando llamadas similares a lo siguiente: getWritableDatabase().insert(TABLE_NAME, "_id", values). Todo esto en conjunto toma aproximadamente 80 segundos para el archivo de prueba de 152 KB (que se reduce a insertar aproximadamente 200 filas).

Cuando comento todas las declaraciones de inserción (pero dejo todo lo demás, como crear, ContentValuesetc.), el mismo archivo toma solo 23 segundos.

¿Es normal que las operaciones de la base de datos tengan una sobrecarga tan grande? ¿Puedo hacer algo al respecto?

benvd
fuente

Respuestas:

190

Debería hacer inserciones por lotes.

Pseudocódigo:

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

Eso aumentó enormemente la velocidad de las inserciones en mis aplicaciones.

Actualización:
@Yuku proporcionó una publicación de blog muy interesante: Android usa inserthelper para inserciones más rápidas en la base de datos sqlite

WarrenFaith
fuente
4
Insertar todos los ContentValues ​​de esta manera solo toma uno o dos segundos. ¡Muchas gracias!
Benvd el
¿Es seguro tener abierta una transacción de larga duración, que cubra toda la duración de la operación de análisis de XML y luego confirmarla al final? ¿O la lista de inserciones debe almacenarse en caché localmente dentro del analizador XML y luego tener una transacción de corta duración abierta y confirmada una vez que se completa el análisis?
Graham Borland
2
envolver mis 60 inserciones con una transacción aumentó el rendimiento 10 veces. envolverlo con una transacción y usar una declaración preparada (SQLiteStatement) lo aumentó 20 veces.
stefs
2
benvd gracias por el comentario. Estaba insertando registros de 20k, tomó aproximadamente 8 minutos, pero después de usar una transacción, solo toma 20 segundos :-)
Bear
2
Esta entrada de blog analiza otra optimización utilizando el InsertHelper casi oculto outofwhatbox.com/blog/2010/12/…
Randy Sugianto 'Yuku'
68

Dado que InsertHelper mencionado por Yuku y Brett está obsoleto ahora (nivel de API 17), parece que la alternativa correcta recomendada por Google es usar SQLiteStatement .

Usé el método de inserción de la base de datos como este:

database.insert(table, null, values);

Después de que también experimenté algunos problemas serios de rendimiento, el siguiente código aceleró mis 500 inserciones de 14.5 segundos a solo 270 ms , ¡increíble!

Así es como usé 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
fuente
14
Un problema que debe evitarse aquí: el índice en bindString se basa en 1 y no en 0
nombre para mostrar el
@qefzec Gracias por esta solución ... Esto acaba de reducir el tiempo de inserción en mi aplicación de 78 segundos a 4 para 900 filas agregadas ..
csanonymus
1
Gracias Señor. 20000 registros con 6 campos de datos cada uno, incluido VARCHAR (80) de 4 minutos a 8 segundos. En realidad, esto debería marcarse como la mejor respuesta, en mi humilde opinión.
TomeeNS
1
¡Excelente! Nuestro punto de referencia en 200 insertos de prueba a la vez con 15 columnas por inserto generó entre 4100% y 10400% de mejora según el dispositivo y la memoria interna / externa. El desempeño anterior habría condenado nuestro proyecto antes de que despegara.
Frank
de 15 minutos a 45
segundos
13

Compilar la declaración de inserción de SQL ayuda a acelerar las cosas. También puede requerir más esfuerzo apuntalar todo y evitar una posible inyección, ya que ahora está todo sobre sus hombros.

Otro enfoque que también puede acelerar las cosas es la clase android.database.DatabaseUtils.InsertHelper poco documentada. Tengo entendido que en realidad envuelve declaraciones de inserción compiladas. Pasar de inserciones con transacciones no compiladas a inserciones con transacciones compiladas fue aproximadamente una ganancia de 3 veces en la velocidad (2ms por inserción a .6ms por inserción) para mis inserciones grandes (200K + entradas) pero simples de SQLite.

Código de muestra:

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
fuente
y cómo agregar ContentValue en iHelp.bind (first_index, thing_1); ?
Vasil Valchev
3

Si la tabla tiene un índice, considere eliminarlo antes de insertar los registros y luego volver a agregarlo después de haber confirmado sus registros.

kaD'argo
fuente
1

Si usa 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;

}

Luego, la función privada para realizar la inserción (aún dentro de su proveedor de contenido):

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


    }
Disyuntor 716
fuente