Вставка тысяч записей контактов с использованием applyBatch происходит медленно - PullRequest
35 голосов
/ 08 апреля 2011

Я разрабатываю приложение, в которое мне нужно вставить много записей контактов. В настоящее время около 600 контактов с общим количеством 6000 телефонных номеров. Самый большой контакт имеет 1800 телефонных номеров.

На сегодняшний день я создал пользовательскую учетную запись для хранения контактов, поэтому пользователь может выбрать ее для просмотра контакта в представлении «Контакты».

Но вставка контактов мучительно медленная. Я вставляю контакты используя ContentResolver.applyBatch. Я пробовал с различными размерами списка ContentProviderOperation (100, 200, 400), но общее время работы составляет ок. тот же самый. Для вставки всех контактов и номеров требуется около 30 минут!

Большинство проблем, связанных с медленной вставкой в ​​SQlite, вызывают транзакции. Но так как я использую метод ContentResolver.applyBatch, я не контролирую это, и я предполагаю, что ContentResolver позаботится об управлении транзакциями для меня.

Итак, на мой вопрос: я делаю что-то не так, или я могу что-нибудь сделать, чтобы ускорить это?

Anders

Изменить: @jcwenger: О, я вижу. Хорошее объяснение!

Итак, мне придется сначала вставить в таблицу raw_contacts, а затем в таблицу данных с именем и номерами. Я потеряю обратную ссылку на raw_id, который я использую в applyBatch.

То есть мне нужно получить все идентификаторы недавно вставленных строк raw_contacts для использования в качестве внешних ключей в таблице данных?

Ответы [ 7 ]

51 голосов
/ 08 апреля 2011

Используйте ContentResolver.bulkInsert (Uri url, ContentValues[] values) вместо ApplyBatch()

ApplyBatch (1) использует транзакции и (2) блокирует ContentProvider один раз для всего пакета, вместо этого блокируя / разблокируя один раз для каждой операции.из-за этого это немного быстрее, чем выполнять их по одному (не в пакетном режиме).

Однако, поскольку каждая операция в пакете может иметь свой URI и т. д., существует огромное количество накладных расходов.,«О, новая операция! Интересно, в какую таблицу она входит ... Здесь я вставлю одну строку ... О, новая операция! Интересно, в какую таблицу она входит ...» до бесконечности.Поскольку большая часть работы по преобразованию URI в таблицы включает в себя множество сравнений строк, очевидно, что это очень медленно.

В отличие от этого, bulkInsert применяет целую кучу значений к одной и той же таблице.Он гласит: «Массовая вставка ... найдите таблицу, хорошо, вставьте! Вставьте! Вставьте! Вставьте! Вставьте!»Гораздо быстрее.

Разумеется, вашему ContentResolver потребуется эффективная реализация bulkInsert.Большинство так и делает, если вы сами не написали это, и в этом случае потребуется немного кода.

10 голосов
/ 27 июля 2011

bulkInsert: для тех, кто заинтересован, вот код, с которым я смог поэкспериментировать. Обратите внимание на то, как мы можем избежать некоторых выделений для int / long / float :), это может сэкономить больше времени.

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;
}
2 голосов
/ 22 декабря 2011

Пример того, как переопределить bulkInsert(), чтобы ускорить вставку кратных, можно найти здесь

1 голос
/ 17 февраля 2016

Вот пример вставки того же количества данных в течение 30 секунд.

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

Журнал: 02-17 12: 48: 45.496 2073-2090 / com.vayosoft.mlab D / BatchInsertionTest ﹕ Запусквставка пакета в: ср 17 фев 12:48:45 GMT + 02: 00 2016 02-17 12: 49: 16.446 2073-2090 / com.vayosoft.mlab D / BatchInsertionTest ﹕ Конец вставки пакета, истек: 00: 30.951

1 голос
/ 05 февраля 2014

Я получил базовое решение для вас, используйте «точки текучести» в пакетной операции .

Обратная сторона использования пакетных операций заключается в том, что большая партия может заблокировать базу данных длядолгое время не позволяющий другим приложениям получать доступ к данным и потенциально вызывать ANR (диалоговые окна «Приложение не отвечает»).

Чтобы избежать таких блокировок базы данных, обязательно вставьте « предел текучести »в партии.Предел доходности указывает поставщику контента, что перед выполнением следующей операции он может зафиксировать изменения, которые уже были сделаны, уступить другим запросам, открыть другую транзакцию и продолжить операции обработки.

Предел доходности не будет автоматически фиксировать транзакцию, но только если в базе данных ожидается другой запрос.Обычно адаптер синхронизации должен вставлять предел текучести в начале каждой последовательности операций необработанного контакта в пакете.См. withYieldAllowed (логическое значение) .

Надеюсь, это может быть полезно для вас.

1 голос
/ 20 апреля 2012

@ jcwenger Сначала, после прочтения вашего поста, я думаю, что причина массового вставления быстрее, чем ApplyBatch, но после прочтения кода поставщика контактов, я так не думаю.1. Вы сказали, что ApplyBatch используют транзакции, да, но bulkInsert также использует транзакции.Вот код этого кода:

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

То есть, массовая вставка также использует переводы. Так что я не думаю, что причина в этом.2. Вы сказали, что bulkInsert применяет целую кучу значений к одной и той же таблице. Извините, я не могу найти связанный код в исходном коде froyo. И я хочу знать, как вы можете найти это? Не могли бы вы сказать мне?

Причина, по которой я думаю, заключается в том, что:

bulkInsert использует mDb.yieldIfContendedSafely (), тогда как applyBatch использует mDb.yieldIfContendedSafely (SLEEP_AFTER_YIELD_DELAY) / * SLEEP_AFTER_YIELD *4000* * * * * * *код SQLiteDatabase.java, я нахожу, что, если установить время в yieldIfContendedSafely, он будет спать, но если вы не установите время, он не будет спать. Вы можете обратиться к приведенному ниже коду, который является частьюкода 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;
}

Я думаю, что причина в том, что bulkInsert работает быстрее, чем applyBatch.

Любой вопрос, пожалуйста, свяжитесь со мной.

0 голосов
/ 05 мая 2017

Только для информации читателей этой темы.

Я столкнулся с проблемой производительности, даже при использовании applyBatch ().В моем случае на одной из таблиц были записаны триггеры базы данных.Я удалил триггеры таблицы и ее бум.Теперь мое приложение вставляет строки с благословением быстрой скорости.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...