34

Estoy desarrollando una aplicación en la que necesito insertar muchas entradas de contactos. En el momento actual, aproximadamente 600 contactos con un total de 6000 números de teléfono. El contacto más grande tiene 1800 números de teléfono.La inserción de miles de entradas de contactos usando applyBatch es lenta

El estado actual es que he creado una cuenta personalizada para guardar los contactos, por lo que el usuario puede seleccionar ver el contacto en la vista de contactos.

Pero la inserción de los contactos es muy lenta. Inserto los contactos usando ContentResolver.applyBatch. He intentado con diferentes tamaños de la lista ContentProviderOperation (100, 200, 400), pero el tiempo total de ejecución es de aprox. lo mismo. ¡Insertar todos los contactos y números toma alrededor de 30 minutos!

La mayoría de los problemas que he encontrado con respecto a la inserción lenta en SQlite muestran las transacciones. Pero como utilizo ContentResolver.applyBatch-method, no controlo esto, y supongo que ContentResolver se ocupa de la gestión de transacciones por mí.

Por lo tanto, a mi pregunta: ¿Estoy haciendo algo mal, o hay algo que pueda hacer para acelerar esto?

Anders

Editar: @jcwenger: Oh, ya veo. ¡Buena explicación!

Así que tendré que insertar primero en la tabla raw_contacts y luego en la tabla de datos con el nombre y los números. Lo que perderé es la referencia anterior al raw_id que uso en applyBatch.

Así que tendré que obtener todas las identificaciones de las filas raw_contacts recientemente insertadas para usarlas como claves foráneas en la tabla de datos?

+1

@Anders, tenga en cuenta que usted puede hacer comentarios en el post de jcwenger directamente con el 'añadir comment' enlace bajo su cargo; puede parecer que no importa ahora, pero las preguntas con una docena de respuestas se vuelven difíciles de manejar si cada respuesta estuviera en la pregunta. (También las publicaciones se convierten en [comunidad wiki] (http://stackoverflow.com/faq#reputation) después de muchas ediciones ([diez para autoediciones, cinco usuarios para otros] (http://meta.stackexchange.com/questions/) 11740/what-are-community-wiki-posts)), donde es imposible obtener más reputación, por lo que actualizar las publicaciones para incluir nueva información es excelente, pero a veces solo bastará con un comentario.) – sarnold

+1

¿Puede proporcionar una mejora de velocidad? ¿factor? dijiste que solía llevarte 30 minutos, ¿qué tal ahora? ¿cuánto tiempo se tarda? – Buffalo

+0

Tiene algún tipo de error en su código. Por ejemplo, si su 'retro-referencia' siempre es 0, eso quiere decir que está insertando un ítem a la vez y bloquearlo por él, es un cambio descendente. Voy a poner ejemplo de insertar la misma cantidad de elementos, que tomó 30 segundos. – Kvant

Respuesta

50

Uso ContentResolver.bulkInsert (Uri url, ContentValues[] values) en lugar de ApplyBatch()

ApplyBatch (1) utiliza transacciones y (2) se bloquea el ContentProvider una vez para todo el lote en lugar de bloqueo/desbloqueo de una vez por operación. debido a esto, es un poco más rápido que hacerlo de uno en uno (sin lotes).

Sin embargo, dado que cada operación en el lote puede tener un URI diferente y así sucesivamente, hay una gran cantidad de sobrecarga. "¡Oh, una nueva operación! Me pregunto a qué mesa va ... Aquí, insertaré una sola fila ... ¡Oh, una nueva operación! Me pregunto a qué mesa va ..." ad infinitium. Dado que la mayor parte del trabajo de convertir los URI en tablas implica muchas comparaciones de cadenas, obviamente es muy lento.

Por el contrario, bulkInsert aplica una pila entera de valores a la misma tabla. Va, "Inserción masiva ... encuentra la tabla, está bien, inserta! Inserta! Inserta! Inserta! Inserta!" Mucho mas rápido.

Por supuesto, requerirá que su ContentResolver implemente bulkInsert de manera eficiente. La mayoría lo hace, a menos que lo haya escrito usted mismo, en cuyo caso le llevará un poco de codificación.

+0

(En respuesta a editar en cuestión, de acuerdo w/@ comentarios de sarnold arriba) Sí, ese es el lado negativo. No recuperas los row_ids individuales, solo obtienes un resumen del "Número insertado": ten en cuenta, por supuesto, que dependiendo de las restricciones de tu tabla, puede que no sea una respuesta de todo o nada. Por lo tanto, si necesita hacer una referencia cruzada de una clave externa, sí, deberá realizarla y consultarla posteriormente. Afortunadamente, la consulta es sorprendentemente rápida en comparación con las inserciones ... La inserción masiva y la consulta posterior aún deberían ser mucho más rápidas en general. – jcwenger

+1

@jswenger y @sarnold. Lo siento pero no recibí la opción de agregar comentario hasta ahora. Soy nuevo aquí y cometí un error al crear la pregunta como usuario no registrado. Ahora, de vuelta a la pregunta original: he implementado la solución, y la primera que no parece hacer una diferencia. En el emulador que es. Luego traté de usar mi dispositivo (HTC Desire), y tengo menos de 3 minutos. Una diferencia notable, pero quiero más ;;) He visto algunas aplicaciones que insertan la misma cantidad de entradas en bases de datos Sqlite "personalizadas" en menos de un minuto. ¿Alguna esperanza de hacer esto con la base de datos de Contactos? – Anders

+0

¿Es bulkInsert() mejor que ContentProviderOperation aquí? –

10

bulkInsert: para los interesados, este es el código con el que pude experimentar. Preste atención a cómo podemos evitar algunas asignaciones para int/long/floats :) esto podría ahorrar más tiempo.

private int doBulkInsertOptimised(Uri uri, ContentValues values[]) { 
    long startTime = System.currentTimeMillis(); 
    long endTime = 0; 
    //TimingInfo timingInfo = new TimingInfo(startTime); 

    SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 

    DatabaseUtils.InsertHelper inserter = 
     new DatabaseUtils.InsertHelper(db, Tables.GUYS); 

    // Get the numeric indexes for each of the columns that we're updating 
    final int guiStrColumn = inserter.getColumnIndex(Guys.STRINGCOLUMNTYPE); 
    final int guyDoubleColumn = inserter.getColumnIndex(Guys.DOUBLECOLUMNTYPE); 
//... 
    final int guyIntColumn = inserter.getColumnIndex(Guys.INTEGERCOLUMUNTYPE); 

    db.beginTransaction(); 
    int numInserted = 0; 
    try { 
     int len = values.length; 
     for (int i = 0; i < len; i++) { 
      inserter.prepareForInsert(); 

      String guyID = (String)(values[i].get(Guys.GUY_ID)); 
      inserter.bind(guiStrColumn, guyID); 


      // convert to double ourselves to save an allocation. 
      double d = ((Number)(values[i].get(Guys.DOUBLECOLUMNTYPE))).doubleValue(); 
      inserter.bind(guyDoubleColumn, lat); 


      // getting the raw Object and converting it int ourselves saves 
      // an allocation (the alternative is ContentValues.getAsInt, which 
      // returns a Integer object) 

      int status = ((Number) values[i].get(Guys.INTEGERCOLUMUNTYPE)).intValue(); 
      inserter.bind(guyIntColumn, status); 

      inserter.execute(); 
     } 
     numInserted = len; 
     db.setTransactionSuccessful(); 
    } finally { 
     db.endTransaction(); 
     inserter.close(); 

     endTime = System.currentTimeMillis(); 

     if (LOGV) { 
      long timeTaken = (endTime - startTime); 
      Log.v(TAG, "Time taken to insert " + values.length + " records was " + timeTaken + 
        " milliseconds " + " or " + (timeTaken/1000) + "seconds"); 
     } 
    } 
    getContext().getContentResolver().notifyChange(uri, null); 
    return numInserted; 
} 
+4

Utilicé transacciones en un método bulkInsert anulado y aceleró mis 600 inserciones de 31 segundos a menos de 1 segundo. Definitivamente recomiendo este enfoque. –

2

Un ejemplo de cómo anular la bulkInsert(), con el fin de acelerar los múltiplos de inserción, se puede encontrar here

+0

gracias! eso fue útil – dum4ll3

1

@jcwenger En un primer momento, después de leer tu post, creo que esa es la razón de bulkInsert es más rápido que ApplyBatch, pero después de leer el código de Contact Provider, no lo creo. 1.Hablas transacciones de uso de ApplyBatch, sí, pero bulkInsert también usa transacciones. Aquí está el código de la misma:

public int bulkInsert(Uri uri, ContentValues[] values) { 
    int numValues = values.length; 
    mDb = mOpenHelper.getWritableDatabase(); 
    mDb.beginTransactionWithListener(this); 
    try { 
     for (int i = 0; i < numValues; i++) { 
      Uri result = insertInTransaction(uri, values[i]); 
      if (result != null) { 
       mNotifyChange = true; 
      } 
      mDb.yieldIfContendedSafely(); 
     } 
     mDb.setTransactionSuccessful(); 
    } finally { 
     mDb.endTransaction(); 
    } 
    onEndTransaction(); 
    return numValues; 
} 

Es decir, bulkInsert también utilizan transations.So No creo que esa es la razón. 2. Has dicho que bulkInsert aplica una pila completa de valores a la misma tabla. Lamento no poder encontrar el código relacionado en el código fuente de froyo. Y quiero saber cómo podrías encontrarlo. ¿Podrías decirme algo? ?

La razón por la que creo es que:

bulkInsert usar mDb.yieldIfContendedSafely() mientras applyBatch utilizar mDb.yieldIfContendedSafely (SLEEP_AFTER_YIELD_DELAY)/* * SLEEP_AFTER_YIELD_DELAY = 4000/

después de leer el código de SQLiteDatabase. java, me parece que, si estableces un tiempo en yieldIfContendedSafely, hará una pausa, pero si no configuras la hora, no se apagará. Puedes consultar el siguiente código, que es una parte del código de SQLiteDatabase. java

private boolean yieldIfContendedHelper(boolean checkFullyYielded, long  sleepAfterYieldDelay) { 
    if (mLock.getQueueLength() == 0) { 
     // Reset the lock acquire time since we know that the thread was willing to yield 
     // the lock at this time. 
     mLockAcquiredWallTime = SystemClock.elapsedRealtime(); 
     mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); 
     return false; 
    } 
    setTransactionSuccessful(); 
    SQLiteTransactionListener transactionListener = mTransactionListener; 
    endTransaction(); 
    if (checkFullyYielded) { 
     if (this.isDbLockedByCurrentThread()) { 
      throw new IllegalStateException(
        "Db locked more than once. yielfIfContended cannot yield"); 
     } 
    } 
    if (sleepAfterYieldDelay > 0) { 
     // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to 
     // check if anyone is using the database. If the database is not contended, 
     // retake the lock and return. 
     long remainingDelay = sleepAfterYieldDelay; 
     while (remainingDelay > 0) { 
      try { 
       Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? 
         remainingDelay : SLEEP_AFTER_YIELD_QUANTUM); 
      } catch (InterruptedException e) { 
       Thread.interrupted(); 
      } 
      remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM; 
      if (mLock.getQueueLength() == 0) { 
       break; 
      } 
     } 
    } 
    beginTransactionWithListener(transactionListener); 
    return true; 
} 

Creo que esa es la razón de que bulkInsert sea más rápido que applyBatch.

Cualquier pregunta, contácteme.

+0

¿Qué versión de Android usaste? He buscado 'bulkInsert' en android2.3.7' ContactsProvider', pero no lo he encontrado use transaction. –

1

Obtengo la solución básica para usted, utilice "puntos de fluencia" en operación por lotes.

La otra cara de la utilización de operaciones por lotes es que un gran lote puede bloquear la base de datos por un largo tiempo la prevención de otras aplicaciones accedan a los datos y causando potencialmente ANR ("Aplicación No responde" cuadros de diálogo.)

Para evitar tales bloqueos de la base de datos, asegúrese de insertar "puntos de rendimiento" en el lote. Un límite de rendimiento indica al proveedor de contenido que antes de ejecutar la siguiente operación puede confirmar los cambios que ya se han realizado, ceder a otras solicitudes, abrir otra transacción y continuar las operaciones de procesamiento.

Un límite de rendimiento no confirmará automáticamente la transacción, pero solo si hay otra solicitud esperando en la base de datos. Normalmente, un adaptador de sincronización debe insertar un punto de fluencia al comienzo de cada secuencia de operación de contacto sin procesar en el lote. Ver withYieldAllowed(boolean).

Espero que pueda serle útil.

0

Este es un ejemplo de cómo insertar la misma cantidad de datos en 30 segundos.

public void testBatchInsertion() throws RemoteException, OperationApplicationException { 
    final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss.SSS"); 
    long startTime = System.currentTimeMillis(); 
    Log.d("BatchInsertionTest", "Starting batch insertion on: " + new Date(startTime)); 

    final int MAX_OPERATIONS_FOR_INSERTION = 200; 
    ArrayList<ContentProviderOperation> ops = new ArrayList<>(); 
    for(int i = 0; i < 600; i++){ 
     generateSampleProviderOperation(ops); 
     if(ops.size() >= MAX_OPERATIONS_FOR_INSERTION){ 
      getContext().getContentResolver().applyBatch(ContactsContract.AUTHORITY,ops); 
      ops.clear(); 
     } 
    } 
    if(ops.size() > 0) 
     getContext().getContentResolver().applyBatch(ContactsContract.AUTHORITY,ops); 
    Log.d("BatchInsertionTest", "End of batch insertion, elapsed: " + FORMATTER.format(new Date(System.currentTimeMillis() - startTime))); 

} 
private void generateSampleProviderOperation(ArrayList<ContentProviderOperation> ops){ 
    int backReference = ops.size(); 
    ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) 
      .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) 
      .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) 
      .withValue(ContactsContract.RawContacts.AGGREGATION_MODE, ContactsContract.RawContacts.AGGREGATION_MODE_DISABLED) 
      .build() 
    ); 
    ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) 
        .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, backReference) 
        .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) 
        .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, "GIVEN_NAME " + (backReference + 1)) 
        .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, "FAMILY_NAME") 
        .build() 
    ); 
    for(int i = 0; i < 10; i++) 
     ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) 
         .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, backReference) 
         .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) 
         .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MAIN) 
         .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, Integer.toString((backReference + 1) * 10 + i)) 
         .build() 
     ); 
} 

el registro: 02-17 12: 48: 45.496 2073-2090/com.vayosoft.mlab D/BatchInsertionTest: A partir de la inserción de lote en: Vie Dic 17 12:48:45 GMT + 02: 00 2016 02-17 12: 49: 16.446 2073-2090/com.vayosoft.Mlab D/BatchInsertionTest: Fin de la inserción por lotes, transcurrido: 00: 30,951

0

Sólo para información de los lectores de este hilo.

que estaba frente a un problema de rendimiento, incluso si se utiliza applyBatch(). En mi caso, hubo desencadenantes de base de datos escritos en una de las tablas. Eliminé los desencadenantes de la tabla y su auge. Ahora mi aplicación inserta filas con bendición de alta velocidad.

Cuestiones relacionadas