web-dev-qa-db-fra.com

L'insertion de milliers d'entrées de contact à l'aide de applyBatch est lente

Je développe une application dans laquelle je dois insérer de nombreuses entrées de contact. À l’heure actuelle, environ 600 contacts avec un total de 6000 numéros de téléphone. Le plus grand contact a 1800 numéros de téléphone.

L'état actuel est que j'ai créé un compte personnalisé pour contenir les contacts, afin que l'utilisateur puisse choisir de voir le contact dans la vue Contacts. 

Mais l'insertion des contacts est péniblement lente. J'insère les contacts à l'aide de ContentResolver.applyBatch. J'ai essayé avec différentes tailles de la liste ContentProviderOperation (100, 200, 400), mais le temps total d'exécution est d'env. le même. Il faut environ 30 minutes pour insérer tous les contacts et les numéros!

La plupart des problèmes que j'ai rencontrés concernant l'insertion lente dans SQlite soulèvent des transactions. Mais depuis que j'utilise la méthode ContentResolver.applyBatch, je ne contrôle pas cela, et je suppose que ContentResolver s'occupe de la gestion des transactions pour moi.

Donc, à ma question: est-ce que je fais quelque chose de mal ou puis-je faire quelque chose pour accélérer le processus?

Anders

Edit: @ Jcwenger: Oh, je vois. Bonne explication!

Donc, je vais d'abord devoir insérer dans la table raw_contacts, puis le datatable avec le nom et les nombres. Ce que je vais perdre, c'est la référence arrière au raw_id que j'utilise dans applyBatch.

Il faudra donc que tous les identifiants des nouvelles lignes raw_contacts insérées soient utilisés comme clés étrangères dans la table de données?

35
Anders

Utilisez ContentResolver.bulkInsert (Uri url, ContentValues[] values) au lieu de ApplyBatch()

ApplyBatch (1) utilise des transactions et (2) verrouille le fournisseur de contenu une fois pour l'ensemble du lot au lieu de le verrouiller/déverrouiller une fois par opération. de ce fait, il est légèrement plus rapide que de les traiter un par un (non groupé).

Cependant, étant donné que chaque opération dans le lot peut avoir un URI différent et ainsi de suite, il existe une énorme quantité de temps système. "Oh, une nouvelle opération! Je me demande dans quelle table elle entre ... Ici, je vais insérer une seule rangée ... Oh, une nouvelle opération! Je me demande quelle table elle entre ..." à l'infini. Étant donné que la plupart des travaux de conversion d'URI en tableaux impliquent de nombreuses comparaisons de chaînes, il est évidemment très lent.

Par contre, bulkInsert applique toute une pile de valeurs à la même table. Il dit: "Insertion en bloc ... trouvez la table, d'accord, insertion! Insertion! Insertion! Insertion! Insertion!" Plus vite.

Bien sûr, votre ContentResolver devra implémenter efficacement bulkInsert. La plupart le font, à moins que vous ne l'ayez écrit vous-même. Dans ce cas, cela nécessitera un peu de codage.

51
jcwenger

bulkInsert: Pour ceux que ça intéresse, voici le code que j'ai pu expérimenter. Faites attention à la manière dont nous pouvons éviter certaines allocations pour int/long/floats :) cela pourrait économiser plus de temps. 

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;
}
10
Viren

On trouvera un exemple de comment remplacer la bulkInsert(), afin d'accélérer l'insertion de multiples, here

2
David-mu

Voici un exemple d'insertion de la même quantité de données dans les 30 secondes.

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

Le journal: 02-17 12: 48: 45.496 2073-2090/com.vayosoft.mlab D/BatchInsertionTest ﹕ Début de l'insertion du lot le: mercredi 17 février à 12h48:45 GMT + 02:00 [2016. ____.] 02-17 12: 49: 16.446 2073-2090/com.vayosoft.mlab D/BatchInsertionTest ﹕ Fin de l'insertion du lot, écoulée: 00: 30.951

1
Kvant

Je reçois la solution de base pour vous, utilisez «Points de rendement» dans opération par lots .

Le revers de l’utilisation des opérations par lots est qu’un lot volumineux peut verrouiller la base de données pendant longtemps, empêchant ainsi d’autres applications d’accéder aux données et d’engendrer des ANR (boîtes de dialogue "Application ne répondant pas").

Pour éviter de tels blocages de la base de données, veillez à insérer " points de rendement " dans le lot. Un point de rendement indique au fournisseur de contenu que, avant d'exécuter l'opération suivante, il peut valider les modifications déjà apportées, céder le droit à d'autres demandes, ouvrir une autre transaction et poursuivre les opérations de traitement. 

Un point de rendement ne validera pas automatiquement la transaction, mais uniquement si une autre demande est en attente dans la base de données. Normalement, un adaptateur de synchronisation doit insérer un point d’élasticité au début de chaque séquence d’opérations de contact brutes du lot. Voir withYieldAllowed (booléen)

J'espère que cela pourra vous être utile.

1
Vrajesh

@jcwenger Au début, après avoir lu votre message, je pense que c'est la raison de . bulkInsert est plus rapide que ApplyBatch, mais après avoir lu le code de Contact Provider, je ne le pense pas. 1. Vous avez dit ApplyBatch utiliser des transactions, oui, mais bulkInsert utilise également des transactions. En voici le code:

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

C'est-à-dire que bulkInsert utilise également des transations.Alors je ne pense pas que ce soit la raison. 2.Vous avez dit bulkInsert applique toute une pile de valeurs à la même table.Je suis désolé de ne pas avoir trouvé code connexe dans le code source de froyo.Et je veux savoir comment tu peux trouver ça?

La raison pour laquelle je pense est que:

bulkInsert utilise mDb.yieldIfContendedSafely () pendant que applyBatch utilise mDb.yieldIfContendedSafely (SLEEP_AFTER_YIELD_DELAY)/* SLEEP_AFTER_YIELD_DELAY = 4000 * /

après avoir lu le code de SQLiteDatabase.Java, je constate que, si vous définissez une heure dans le paramètre yieldIfContendedSafely, elle se mettra en veille, mais si vous ne définissez pas cette heure, elle ne dormira pas. Vous pouvez vous reporter au code ci-dessous, qui est un morceau de code 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;
}

Je pense que c'est la raison pour laquelle bulkInsert est plus rapide que applyBatch.

Toute question s'il vous plaît contactez-moi.

1
jiangyan.lily

Juste pour l'information des lecteurs de ce fil.

Je rencontrais des problèmes de performances même si j'utilisais applyBatch (). Dans mon cas, des déclencheurs de base de données étaient écrits sur l'une des tables. J'ai supprimé les déclencheurs de la table et de son boom. Maintenant, mon application insère des lignes avec une vitesse rapide.

0
Sahil