Banco de dados Android SQLite: inserção lenta

91

Preciso analisar um arquivo XML bastante grande (variando entre cerca de cem kilobytes e várias centenas de kilobytes), que estou fazendo usando Xml#parse(String, ContentHandler) . No momento, estou testando isso com um arquivo de 152 KB.

Durante a análise, também insiro os dados em um banco de dados SQLite usando chamadas semelhantes às seguintes: getWritableDatabase().insert(TABLE_NAME, "_id", values) . Tudo isso junto leva cerca de 80 segundos para o arquivo de teste de 152 KB (que se resume a inserir cerca de 200 linhas).

Quando comento todas as instruções de inserção (mas deixo todo o resto, como criar ContentValuesetc.), o mesmo arquivo leva apenas 23 segundos.

É normal que as operações do banco de dados tenham uma sobrecarga tão grande? Posso fazer algo sobre isso?

Benvd
fonte

Respostas:

190

Você deve fazer inserções em lote.

Pseudo-código:

db.beginTransaction();
for (entry : listOfEntries) {
    db.insert(entry);
}
db.setTransactionSuccessful();
db.endTransaction();

Isso aumentou extremamente a velocidade de inserções em meus aplicativos.

Atualização:
@Yuku forneceu uma postagem de blog muito interessante: Android usando inserthelper para inserções mais rápidas no banco de dados sqlite

WarrenFaith
fonte
4
Inserir todos os ContentValues ​​dessa forma leva apenas um ou dois segundos. Muito obrigado!
Benvd
É seguro ter uma transação de longa execução aberta, cobrindo toda a duração da operação de análise XML e, em seguida, confirmá-la no final? Ou a lista de inserções deve ser armazenada em cache localmente no analisador XML e, em seguida, ter uma transação de curta duração aberta e confirmada quando a análise for concluída?
Graham Borland
2
envolver minhas 60 inserções com uma transação aumentou o desempenho em 10x. envolvê-lo com uma transação e usar uma instrução preparada (SQLiteStatement) aumentou 20x!
stefs de
2
benvd obrigado pelo comentário. Eu estava inserindo 20k registros, demorou cerca de 8 minutos, mas depois de usar uma transação levou apenas 20 segundos :-)
Bear
2
Esta entrada de blog discute outra otimização usando o quase oculto InsertHelper outofwhatbox.com/blog/2010/12/…
Randy Sugianto 'Yuku'
68

Como o InsertHelper mencionado por Yuku e Brett está obsoleto agora (API de nível 17), parece que a alternativa certa recomendada pelo Google é usar SQLiteStatement .

Eu usei o método de inserção de banco de dados assim:

database.insert(table, null, values);

Depois que também experimentei alguns problemas sérios de desempenho, o código a seguir acelerou minhas 500 inserções de 14,5 segundos para apenas 270 ms , incrível!

Aqui está como usei SQLiteStatement:

private void insertTestData() {
    String sql = "insert into producttable (name, description, price, stock_available) values (?, ?, ?, ?);";

    dbHandler.getWritableDatabase();
    database.beginTransaction();
    SQLiteStatement stmt = database.compileStatement(sql);

    for (int i = 0; i < NUMBER_OF_ROWS; i++) {
        //generate some values

        stmt.bindString(1, randomName);
        stmt.bindString(2, randomDescription);
        stmt.bindDouble(3, randomPrice);
        stmt.bindLong(4, randomNumber);

        long entryID = stmt.executeInsert();
        stmt.clearBindings();
    }

    database.setTransactionSuccessful();
    database.endTransaction();

    dbHandler.close();
}
qefzec
fonte
14
Um problema a evitar aqui: o índice em bindString é baseado em 1 e não em 0
Nome de exibição
@qefzec Obrigado por esta solução ..
Isso
1
Obrigado senhor. 20.000 registros com 6 campos de dados cada, incluindo VARCHAR (80) de 4 minutos a 8 segundos. Na verdade, isso deve ser marcado como a melhor resposta, IMHO.
TomeeNS
1
Ótimo! Nosso benchmark em 200 inserções de teste por vez com 15 colunas por inserção gerou entre 4100% e 10400% de melhoria dependendo do dispositivo e da memória interna / externa. O desempenho anterior teria condenado nosso projeto antes que ele decolasse.
Frank
de 15min a 45s para 13600 linhas
DoruChidean
13

Compilar a instrução insert do sql ajuda a acelerar as coisas. Também pode exigir mais esforço para escorar tudo e evitar uma possível injeção, uma vez que agora está tudo em seus ombros.

Outra abordagem que também pode acelerar as coisas é a classe android.database.DatabaseUtils.InsertHelper pouco documentada. Meu entendimento é que ele realmente envolve instruções de inserção compiladas. Ir de inserções transacionadas não compiladas para inserções transacionadas compiladas teve um ganho de velocidade de cerca de 3x (2 ms por inserção a 0,6 ms por inserção) para minhas grandes (200K + entradas), mas inserções simples de SQLite.

Código de amostra:

SQLiteDatabse db = getWriteableDatabase();

//use the db you would normally use for db.insert, and the "table_name"
//is the same one you would use in db.insert()
InsertHelper iHelp = new InsertHelper(db, "table_name");

//Get the indices you need to bind data to
//Similar to Cursor.getColumnIndex("col_name");                 
int first_index = iHelp.getColumnIndex("first");
int last_index = iHelp.getColumnIndex("last");

try
{
   db.beginTransaction();
   for(int i=0 ; i<num_things ; ++i)
   {
       //need to tell the helper you are inserting (rather than replacing)
       iHelp.prepareForInsert();

       //do the equivalent of ContentValues.put("field","value") here
       iHelp.bind(first_index, thing_1);
       iHelp.bind(last_index, thing_2);

       //the db.insert() equilvalent
       iHelp.execute();
   }
   db.setTransactionSuccessful();
}
finally
{
    db.endTransaction();
}
db.close();
Brett
fonte
e como adicionar ContentValue em iHelp.bind (first_index, thing_1); ?
Vasil Valchev
3

Se a tabela tiver um índice, considere descartá-lo antes de inserir os registros e, em seguida, adicioná-lo de volta depois de confirmar seus registros.

KaD'argo
fonte
1

Se estiver usando um ContentProvider:

@Override
public int bulkInsert(Uri uri, ContentValues[] bulkinsertvalues) {

    int QueryType = sUriMatcher.match(uri);
    int returnValue=0;
    SQLiteDatabase db = mOpenHelper.getWritableDatabase();

     switch (QueryType) {

         case SOME_URI_IM_LOOKING_FOR: //replace this with your real URI

            db.beginTransaction();

            for (int i = 0; i < bulkinsertvalues.length; i++) {
                //get an individual result from the array of ContentValues
                ContentValues values = bulkinsertvalues[i];
                //insert this record into the local SQLite database using a private function you create, "insertIndividualRecord" (replace with a better function name)
                insertIndividualRecord(uri, values);    
            }

            db.setTransactionSuccessful();
            db.endTransaction();                 

            break;  

         default:
             throw new IllegalArgumentException("Unknown URI " + uri);

     }    

    return returnValue;

}

Em seguida, a função privada para realizar a inserção (ainda dentro do seu provedor de conteúdo):

       private Uri insertIndividualRecord(Uri uri, ContentValues values){

            //see content provider documentation if this is confusing
            if (sUriMatcher.match(uri) != THE_CONSTANT_IM_LOOKING_FOR) {
                throw new IllegalArgumentException("Unknown URI " + uri);
            }

            //example validation if you have a field called "name" in your database
            if (values.containsKey(YOUR_CONSTANT_FOR_NAME) == false) {
                values.put(YOUR_CONSTANT_FOR_NAME, "");
            }

            //******add all your other validations

            //**********

           //time to insert records into your local SQLite database
           SQLiteDatabase db = mOpenHelper.getWritableDatabase();
           long rowId = db.insert(YOUR_TABLE_NAME, null, values);           

           if (rowId > 0) {
               Uri myUri = ContentUris.withAppendedId(MY_INSERT_URI, rowId);
               getContext().getContentResolver().notifyChange(myUri, null);

               return myUri;
           }


           throw new SQLException("Failed to insert row into " + uri);


    }
CircuitBreaker716
fonte