web-dev-qa-db-ja.com

applyBatchを使用した数千の連絡先エントリの挿入が遅い

多数の連絡先エントリを挿入する必要があるアプリケーションを開発しています。現在、約600の連絡先があり、合計6000の電話番号があります。最大の連絡先は1800の電話番号です。

今日のステータスは、連絡先を保持するカスタムアカウントを作成したため、ユーザーは連絡先ビューで連絡先を表示することを選択できます。

しかし、連絡先の挿入は非常に遅いです。 ContentResolver.applyBatchを使用して連絡先を挿入します。さまざまなサイズのContentProviderOperationリスト(100、200、400)を試してみましたが、実行時間の合計は約です。同じ。すべての連絡先と番号を挿入するには、約30分かかります。

SQliteへの遅い挿入に関して私が見つけたほとんどの問題は、トランザクションを引き起こします。しかし、私はContentResolver.applyBatch-methodを使用しているので、これを制御していません。ContentResolverがトランザクション管理を担当していると思います。

だから、私の質問に:私は何か間違っているのですか、これをスピードアップするために私ができることはありますか?

アンダース

編集: @jcwenger:ああ、なるほど。いい説明だ!

したがって、最初にraw_contactsテーブルに挿入し、次にデータテーブルに名前と番号を挿入する必要があります。私が失うものは、applyBatchで使用するraw_idへの後方参照です。

では、新しく挿入されたraw_contacts行のすべてのIDを取得して、データテーブルの外部キーとして使用する必要がありますか?

35
Anders

ContentResolver.bulkInsert (Uri url, ContentValues[] values)の代わりにApplyBatch()を使用します

ApplyBatchは(1)トランザクションを使用し、(2)操作ごとに1回ロック/ロック解除するのではなく、バッチ全体に対して1回ContentProviderをロックします。このため、一度に1つずつ実行するよりも少し高速です(バッチ処理なし)。

ただし、バッチ内の各オペレーションには異なるURIなどが含まれる可能性があるため、オーバーヘッドが非常に大きくなります。 「ああ、新しい操作です。どのテーブルに入るのだろう...ここで、1行挿入します...ああ、新しい操作!それはどのテーブルになるのだろう...」無限です。 URIをテーブルに変換する作業の多くは、多くの文字列比較を伴うため、明らかに非常に低速です。

対照的に、bulkInsertは値の山全体を同じテーブルに適用します。 「一括挿入...テーブルを見つけて、挿入!挿入!挿入!挿入!挿入!挿入!」はるかに高速。

もちろん、contentResolverがbulkInsertを効率的に実装する必要があります。自分で書いた場合を除いて、ほとんどの場合はそうなります。その場合、コーディングが少し必要になります。

51
jcwenger

bulkInsert:興味のある方のために、ここで私が実験できたコードを示します。 int/long/floatsの割り当てを回避する方法に注意してください。これにより、時間を節約できます。

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

複数挿入を高速化するためにbulkInsert()をオーバーライドする方法の例は、次のとおりです here

2
David-mu

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:バッチ挿入を開始:Wed Feb 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
Kvant

私はあなたのための基本的な解決策を得ます、バッチ操作で「降伏点」を使用します。

バッチ処理を使用する場合の反対に、大きなバッチが長時間データベースをロックし、他のアプリケーションがデータにアクセスできなくなり、ANR(「アプリケーションが応答しない」ダイアログ)を引き起こす可能性があります。

このようなデータベースのロックアップを回避するには、バッチに「yield points」を挿入してください。生成ポイントは、次の操作を実行する前に、すでに行われた変更をコミットし、他の要求に変換し、別のトランザクションを開いて操作の処理を続行できることをコンテンツプロバイダーに示します。

降伏点はトランザクションを自動的にコミットしませんが、データベースで待機している別の要求がある場合のみです。通常、同期アダプターは、バッチの各raw接点操作シーケンスの先頭に降伏点を挿入する必要があります。 withYieldAllowed(boolean) を参照してください。

お役に立てれば幸いです。

1
Vrajesh

@jcwenger最初に、あなたの投稿を読んだ後、bulkInsertがApplyBatchより速いのはそのためだと思いますが、Contact Providerのコードを読んだ後はそうは思いません。 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で時間を設定するとスリープしますが、時間を設定しないとスリープしません。以下のコードを参照してください。 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よりも速い理由です。

ご不明な点はご連絡ください。

1
jiangyan.lily

このスレッドの読者の情報のためだけに。

ApplyBatch()を使用していても、パフォーマンスの問題に直面していました。私の場合、テーブルの1つにデータベーストリガーが記述されていました。テーブルとそのブームのトリガーを削除しました。今、私のアプリは素晴らしいスピードで行を挿入します。

0
Pioneer