使用applyBatch插入数千个联系人条目速度较慢

36
我正在开发一个应用程序,需要插入大量联系人条目。当前有大约600个联系人和总共6000个电话号码。最大的联系人具有1800个电话号码。
目前我已经创建了一个自定义帐户来保存联系人,以便用户可以选择在联系人视图中查看联系人。
但是插入联系人非常缓慢。我使用ContentResolver.applyBatch插入联系人。我尝试过不同大小的ContentProviderOperation列表(100、200、400),但总运行时间几乎相同。插入所有联系人和号码需要大约30分钟!
我发现关于SQLite慢速插入的大多数问题都涉及到事务。但由于我使用ContentResolver.applyBatch方法,我无法控制这一点,并且我会认为ContentResolver会为我处理事务管理。
因此,我的问题是:我是否做错了什么,或者有什么方法可以加快速度?
安德斯
编辑: @jcwenger: 哦,我明白了。好的解释!
那么我将首先插入raw_contacts表中,然后再插入包含姓名和号码的datatable中。我失去的是对我在applyBatch中使用的raw_id的反向引用。
所以我必须获取新插入的raw_contacts行的所有id,以在数据表中用作外键?

1
@Anders,注意你可以使用jcwenger的帖子下方的“添加评论”链接直接对其进行评论;现在可能看起来不重要,但是如果每个回复都在问题中,那么有十几个答案的问题将变得难以处理。 (此外,经过多次编辑(自我编辑为10次,其他用户为5次),帖子将成为社区wiki,在其中无法获得更多声望--因此更新帖子以包含新信息非常好,但有时只需发表评论即可。) - sarnold
你的代码中存在某种错误。例如,如果你的“反向引用”始终为0,那就意味着你一次只插入一个项目并锁定了数据库,这是一种下降。我将提供插入相同数量项目的示例,它只需要30秒钟。 - Yan
7个回答

53

使用ContentResolver.bulkInsert (Uri url, ContentValues[] values)代替ApplyBatch()

ApplyBatch()使用事务,并将整个批处理期间的ContentProvider锁定,而不是每个操作锁定/解锁一次。因此,它比逐个执行(非批处理)操作稍快。

然而,由于批处理中的每个操作可以具有不同的URI等信息,因此存在大量开销。 "哦,一个新的操作!我想知道它应该插入哪个表...好吧,在这里,我会插入一行...哦,一个新的操作!我想知道它应该插入哪个表..." 如此反复。由于将URI转换为表的大部分工作涉及大量字符串比较,因此显然非常慢。

相比之下,bulkInsert将一堆值应用于同一个表格。它会执行"批量插入...找到表,好的,插入!插入!插入!插入!插入!",速度更快。

当然,这需要您的ContentResolver有效地实现bulkInsert。大多数情况下都有效,除非您自己编写了它,这将需要进行一些编码。


1
@jswenger和@sarnold。非常抱歉,我直到现在才发现添加评论选项。我是新来的,并且犯了一个错误,以未注册用户的身份创建了问题。现在回到原始问题: 我实施了解决方案,起初似乎没有什么区别。在模拟器上是这样的。然后我尝试使用我的设备(HTC Desire),现在只需要3分钟。这是一个显着的差异,但我想要更多!;)我看到一些应用程序在不到一分钟的时间内向“自定义”Sqlite数据库插入相同数量的条目。有希望在联系人数据库中做到这一点吗? - Anders
这里使用bulkInsert()比ContentProviderOperation更好吗? - IgorGanapolsky
@Igor Ganapolsky @jcwenger的解释明确表示“是的”。 - Anuj
3
你能否指出applyBatch()方法中具体启动事务的位置?默认的applyBatch()方法 仅仅调用 传递的ContentProviderOperations上的apply()方法,这个方法会在内容提供者中调用 insert/update/delete 操作。 - Vasile Jureschi
你错了。这个操作是为大量字段处理而设计的。它对你来说速度慢的原因是因为你没有正确使用它。请参考下面的示例(操作需要30秒)。 - Yan
显示剩余2条评论

10

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

4
我在重写的bulkInsert方法中使用了事务,这将我的600个插入操作的时间从31秒缩短到不到1秒。我强烈推荐采用这种方法。 - Austyn Mahoney

2

在如何覆盖bulkInsert()以加快多次插入的示例中,可以在这里找到。


1

这是一个在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﹕开始批量插入:2016年2月17日星期三12:48:45 GMT+02:00 02-17 12:49:16.446 2073-2090/com.vayosoft.mlab D/BatchInsertionTest﹕批量插入结束,耗时:00:30.951


1

@jcwenger 首先,在阅读了您的帖子后,我认为bulkInsert比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;
}

也就是说,bulkInsert 也使用事务。所以我不认为这是原因。 2.你说 bulkInsert 将一堆值应用于同一个表中。很抱歉,我在 froyo 的源代码中找不到相关的代码。我想知道你是如何找到的?你能告诉我吗?

我认为原因是:

bulkInsert 使用 mDb.yieldIfContendedSafely(),而 applyBatch 使用 mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)/*SLEEP_AFTER_YIELD_DELAY = 4000*/。

阅读 SQLiteDatabase.java 的代码后,我发现,如果在 yieldIfContendedSafely 中设置了时间,它将会休眠,但如果没有设置时间,它将不会休眠。你可以参考下面的代码片段:

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更快的原因。

如有任何问题,请与我联系。


你使用了哪个Android版本?我在Android 2.3.7的ContactsProvider中查找了bulkInsert,但没有发现它使用事务。 - Frank Cheng

1
我为您提供一个基本解决方案,使用“yield points”进行批量操作。使用批处理操作的反面是,大批量操作可能会长时间锁定数据库,防止其他应用程序访问数据,可能会导致ANR(“应用程序无响应”对话框)。为避免数据库的这种锁定,请确保在批处理中插入“yield points”。yield point表示向内容提供者指示,在执行下一个操作之前,可以提交已经进行的更改,等待其他请求,打开另一个事务并继续处理操作。yield point不会自动提交事务,只有在数据库上有另一个请求等待时才会提交。通常,同步适配器应在批处理中每个原始联系人操作序列的开头插入yield point。详见withYieldAllowed(boolean)。希望对您有所帮助。

0

仅供本帖读者参考。

即使使用applyBatch(),我也遇到了性能问题。 在我的情况下,有一个表上编写了数据库触发器。 我删除了该表的触发器,效果显著。 现在我的应用程序以惊人的速度插入行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接