【问题标题】:Android SQLite database: slow insertionAndroid SQLite 数据库:插入速度慢
【发布时间】:2014-08-10 02:01:15
【问题描述】:

我需要使用Xml#parse(String, ContentHandler) 解析一个相当大的XML 文件(大约在100 KB 到几百KB 之间变化)。我目前正在用一个 152KB 的文件对此进行测试。

在解析过程中,我还使用类似于以下内容的调用将数据插入 SQLite 数据库:getWritableDatabase().insert(TABLE_NAME, "_id", values)。对于 152KB 的测试文件,所有这些加起来大约需要 80 秒(归结为插入大约 200 行)。

当我注释掉所有插入语句(但保留其他所有内容,例如创建 ContentValues 等)时,同一个文件只需要 23 秒。

数据库操作有这么大的开销是正常的吗?我能做些什么吗?

【问题讨论】:

    标签: android database performance sqlite insert


    【解决方案1】:

    你应该做批量插入。

    伪代码:

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

    这极大地提高了我的应用程序中的插入速度。

    更新:
    @Yuku 提供了一篇非常有趣的博文:Android using inserthelper for faster insertions into sqlite database

    【讨论】:

    • 以这种方式插入所有 ContentValues 只需要一两秒钟。非常感谢!
    • 打开一个长时间运行的事务是否安全,覆盖 XML 解析操作的整个持续时间,然后在最后提交它?还是应该在 XML 解析器中本地缓存插入列表,然后在解析完成后打开并提交一个短暂的事务?
    • 用事务包装我的 60 个插入使性能提高了 10 倍。用事务包装它并使用准备好的语句(SQLiteStatement)将它增加了 20 倍!
    • benvd 感谢您的评论。我插入了 20k 条记录,大约需要 8 分钟,但使用事务后只需要 20 秒 :-)
    • 这篇博文讨论了另一个使用几乎隐藏的 InsertHelper outofwhatbox.com/blog/2010/12/… 的优化
    【解决方案2】:

    如果表上有索引,请考虑在插入记录之前将其删除,然后在提交记录后将其添加回来。

    【讨论】:

      【解决方案3】:

      编译 sql insert 语句有助于加快速度。它还可能需要更多的努力来支撑一切并防止可能的注射,因为现在一切都在你的肩上。

      另一种可以加快速度的方法是文档不足的 android.database.DatabaseUtils.InsertHelper 类。我的理解是它实际上包装了编译的插入语句。对于我的大型(200K+ 条目)但简单的 SQLite 插入,从非编译事务插入到编译事务插入的速度大约提高了 3 倍(每次插入 2 毫秒到每次插入 0.6 毫秒)。

      示例代码:

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

      【讨论】:

      • 以及如何在 iHelp.bind(first_index, thing_1) 中添加 ContentValue; ?
      【解决方案4】:

      如果使用 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;
      
      }
      

      然后是执行插入的私有函数(仍在您的内容提供者内部):

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

      【讨论】:

        【解决方案5】:

        由于 Yuku 和 Brett 提到的 InsertHelper 现在是 deprecated(API 级别 17),看来 Google 推荐的正确替代方法是使用 SQLiteStatement

        我是这样使用数据库插入方法的:

        database.insert(table, null, values);
        

        在我也遇到了一些严重的性能问题之后,以下代码将我的 500 次插入从 14.5 秒 加快到只有 270 毫秒,太棒了!

        这是我使用 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();
        }
        

        【讨论】:

        • 这里要避免的一个问题:bindString 中的索引是基于 1 而不是基于 0
        • @qefzec 感谢您的解决方案。这只是将我的应用程序中的插入时间从 78 秒降至 4 秒,添加了 900 行。
        • 谢谢先生。 20000 条记录,每条记录有 6 个数据字段,包括 VARCHAR(80) 从 4 分钟到 8 秒。实际上,这应该被标记为最佳答案,恕我直言。
        • 太棒了!我们一次对 200 个测试插入进行基准测试,每个插入 15 列,根据设备和内部/外部存储器的不同,产生了 4100% 到 10400% 的改进。之前的表现会在我们的项目启动之前就注定要失败。
        • 13600 行从 15 分钟缩短到 45 秒
        猜你喜欢
        • 2022-01-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-03-14
        相关资源
        最近更新 更多