以下是我在使用SQLite过程中,总结的一些优化建议。
初始的数据如下,时间单位都是ms。
1.一张user表,只有name和age两个字段。
2.同时开启三条线程测试。
3.分别测试insert、update、delete语句的执行速度。
执行1000条insert语句,结果如下:
10-30 14:36:40.436 13041-13771/com.zcbin.dbstudy D/dbTest: insert time:18934
10-30 14:36:40.496 13041-13772/com.zcbin.dbstudy D/dbTest: insert time:18996
10-30 14:36:40.522 13041-13770/com.zcbin.dbstudy D/dbTest: insert time:19023
执行1000条update语句,结果如下
10-30 14:37:26.213 13041-13771/com.zcbin.dbstudy D/dbTest: update time:23824
10-30 14:37:26.247 13041-13772/com.zcbin.dbstudy D/dbTest: update time:23858
10-30 14:37:26.324 13041-13770/com.zcbin.dbstudy D/dbTest: update time:23934
执行1000条delete语句,结果如下:
10-30 14:37:57.998 13041-13771/com.zcbin.dbstudy D/dbTest: delete time:18479
10-30 14:37:58.012 13041-13770/com.zcbin.dbstudy D/dbTest: delete time:18489
10-30 14:37:58.026 13041-13772/com.zcbin.dbstudy D/dbTest: delete time:18503
使用MultiThread模式
SQLite有三种线程模式,下图是官网的说明:
线程模式SQLite的多线程是通过Write-Ahead Logging实现的,当多个线程写数据库时,会同步执行,不过这个同步是指在日志文件中,最后会将日志文件的结果同步会数据库。
也就是说将写数据库的结果预先在日志文件中处理了,加快了执行速度。
Android中,如何使用MultiThread模式呢?SQLiteDataBase中有一个方法enableWriteAheadLogging()
,这个方法允许数据库使用Write-Ahead Logging。
看下测试效果:
执行1000条insert语句:
10-30 14:46:05.950 15313-15434/com.zcbin.dbstudy D/dbTest: insert time:6415
10-30 14:46:05.956 15313-15435/com.zcbin.dbstudy D/dbTest: insert time:6418
10-30 14:46:05.957 15313-15436/com.zcbin.dbstudy D/dbTest: insert time:6418
执行1000条update语句:
10-30 14:47:04.680 15313-15435/com.zcbin.dbstudy D/dbTest: update time:15033
10-30 14:47:04.791 15313-15434/com.zcbin.dbstudy D/dbTest: update time:15145
10-30 14:47:04.798 15313-15436/com.zcbin.dbstudy D/dbTest: update time:15150
执行1000条delete语句:
10-30 14:47:26.596 15313-15435/com.zcbin.dbstudy D/dbTest: delete time:9657
10-30 14:47:26.601 15313-15436/com.zcbin.dbstudy D/dbTest: delete time:9658
10-30 14:47:26.603 15313-15434/com.zcbin.dbstudy D/dbTest: delete time:9661
可以看到,insert提高了3倍,delete语句都提高了2倍,update稍微少一点,从23000减少到15000级。说明执行enableWriteAheadLogging()
确实能有效提升SQLite执行速度。
使用事务
SQLite默认一条语句就是一个事务,也就是说,有多条insert语句时,每条语句都是一个事务,都会去竞争锁。在竞争锁这里消耗了很多时间。如果对SQLite事务还不了解,可以看看这篇文章
典型的事务开启,使用如下代码:
try {
sqLiteDatabase.beginTransaction();
for (int i = start; i < sum; i++) {
sqLiteDatabase.execSQL(sql, new String[]{"insertname", i+""});
}
sqLiteDatabase.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
sqLiteDatabase.endTransaction();
}
暂时不执行enableWriteAheadLogging()
,单独测试一下使用事务的效果:
执行1000条insert语句:
10-30 14:57:09.424 16653-16701/com.zcbin.dbstudy D/dbTest: insert time:77
10-30 14:57:09.485 16653-16703/com.zcbin.dbstudy D/dbTest: insert time:136
10-30 14:57:09.557 16653-16702/com.zcbin.dbstudy D/dbTest: insert time:208
执行1000条update语句:
10-30 14:57:36.414 16653-16701/com.zcbin.dbstudy D/dbTest: update time:708
10-30 14:57:37.149 16653-16703/com.zcbin.dbstudy D/dbTest: update time:1443
10-30 14:57:37.863 16653-16702/com.zcbin.dbstudy D/dbTest: update time:2157
执行1000条delete语句:
10-30 14:57:51.088 16653-16701/com.zcbin.dbstudy D/dbTest: delete time:578
10-30 14:57:51.473 16653-16703/com.zcbin.dbstudy D/dbTest: delete time:963
10-30 14:57:51.645 16653-16702/com.zcbin.dbstudy D/dbTest: delete time:1134
可以看到,令人惊讶的效果。开启事务后,不存在锁的竞争,效率一下子提升上去了。所以,在平常的SQLite操作时,写数据库的语句都建议显示的去处理事务,特别是批量执行写数据库语句时。
使用线程
Android中,如果阻塞了UI线程,会给用户带来卡顿的感觉。所以耗时的任务都应该开启异步线程去处理。(当然了,如果仅仅是执行简单的语句,比如从一张简单表里查数据,没必要开启额外的线程。)
多线程操作数据库有一个问题。当你在A线程执行完后close掉数据库,万一B线程没执行完,但你已经close掉了,这个B线程的数据库操作就会抛异常。这种情况下可以给连接计数,每当有线程获得连接,计数加一,每当线程close,计数减一。只有计数为0时,才真正关闭数据库。代码如下:
public class SQLHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "TEST";
private static final int DB_VERSION = 1;
// 使用AtomicInteger当做计数器,保证线程安全
private AtomicInteger dbCount = new AtomicInteger(0);
private SQLHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
/**
* 静态内部类实现单例
*/
public static SQLHelper getInstance() {
SQLHelper instance = SingleHolder.INSTANCE;
// 每次getInstance时,连接加一
instance.dbCount.incrementAndGet();
return instance;
}
private static class SingleHolder {
private static final SQLHelper INSTANCE = new SQLHelper(Application.getContext());
}
public void closeDB() {
// 每当调用closeDB方法时,计数减一,减为0时,真正关闭数据库连接
if (dbCount.decrementAndGet() == 0) {
super.close();
}
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
发现在有些项目中,数据库连接直接是不关闭的,使用中也没有发现很大的问题。确实频繁去open、close数据库也是一件耗时的事情,如果应用对数据库依赖较大的话,不关闭连接可能是更好的解决办法。
关于索引
索引是一种用来在某种条件下加速查询的结构。索引是一种特殊的查找表,数据库搜索引擎用来加快数据检索。简单说,索引是一个指向表中数据的指针。
如果给某张表的字段建立了索引,可以加快数据的检索速度。比如一份通讯录,给name建立索引,那么搜索name时,速度提升很大,特别是当通讯录数据量较大的时候。
然而索引有两个主要的缺点:
1. 数据改变时,索引也会改变。比如你执行insert增加数据时,SQLite会自行更新索引,会有一些性能损耗。因此,如果数据经常发生变化,建立索引的代价就很高。
- 索引需要占物理空间,除了数据表占据数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要额空间就会更大。
所以,索引的使用要谨慎。一般而言,需要经常查询的,数据量大的,更改较少的表适合建立索引。比如企业的通讯录。
SQLiteStatement
这部分内容摘自这篇文章。
SQLite想要执行操作,需要将程序中的SQL语句编译成对应的SQLiteStatement,比如" select * from table1 ",每执行一次都需要将这个String类型的SQL语句转换成SQLiteStatement。如下insert的操作最终都是将ContentValues转成SQLiteStatementi:
public long insertWithOnConflict(String table, String nullColumnHack,
ContentValues initialValues, int conflictAlgorithm) {
// 省略部份代码
SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeInsert();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}
对于批量处理插入或者更新的操作,我们可以重用SQLiteStatement,使用SQLiteDatabase的beginTransaction()方法开启一个事务,样例如下:
try
{
sqLiteDatabase.beginTransaction();
SQLiteStatement stat = sqLiteDatabase.compileStatement(insertSQL);
// 插入10000次
for (int i = 0; i < 10000; i++)
{
stat.bindLong(1, 123456);
stat.bindString(2, "test");
stat.executeInsert();
}
sqLiteDatabase.setTransactionSuccessful();
}
catch (SQLException e)
{
e.printStackTrace();
}
finally
{
// 结束
sqLiteDatabase.endTransaction();
sqLiteDatabase.close();
}
从数据上看,第四种方式使用SQLiteStatement最快,不过只要添加了事务(或者说只需要一个事务,不是每条插入都使用事务),后三种方式的差别并不大。所以针过这个题目的插入的优化可以通过“SQLiteStatement+事务”的方式显著提高效率。
使用SQLiteStatement只有在大量操作的时候才有意义,单条语句没必要使用SQLiteStatement。GreenDao封装的执行方法中就使用到了SQLiteStatement,感兴趣可以去看下源码。
测试结果
网友评论