Как ускорить insert в SQLite

в 12:54, , рубрики: android development, optimization, sqlite, базы данных, Песочница, Разработка под android, метки: , , , ,

Доброго времени суток. В своем первом Android приложении я сразу столкнулся с необходимостью работать с БД. Мне необходимо было предоставить своим пользователям стартовый набор данных(около 5000-6000 записей), с которым они смогут работать «из коробки». Было принято решение прикрепить к приложению текстовый файл с данными в виде JSON, при первом запуске распарсить их и положить в базу. О том, как делать это неправильно и как потом радоваться приросту производительности после рефакторинга, можно прочесть дальше.

Я не хочу рассказывать длинную историю о том, через какие тернии пришлось пройти человеку, пишущему свое первое приложение на Java, да и еще сразу под Android. По-моему, лучше не утомлять вас историями, а сразу перейти к делу. Ведь именно за ответом вы сюда заглянули?

Итак, что было. По наивности реализован DBHelper через синглтон, и точно так же хранился объект БД:

public class DBHelper extends SQLiteOpenHelper {
...
private static DBHelper instance;
private static SQLiteDatabase db;

	public static DBHelper getInstance() {
		if (instance == null)
			instance = new DBHelper(Pleazzme.getAppContext());
		return instance;
	}

	public static SQLiteDatabase getDB() {
		if (db == null)
			db = getInstance().getWritableDatabase();
		return db;
	}
...
}

В отдельном AsyncTask`е запускался парсер, который в цикле давал мне объект с методом save():

...
	public void save() {
		ContentValues values = new ContentValues();
		values.put(id, Id);
		values.put(name, Name);
		values.put(categoriesIds, App.gson.toJson(CategoriesIds));
		values.put(datecreated, DateCreated.getTime());
		Document.save();
		values.put(document_id, Document.getId());
		values.put(hasbarcode, hasBarcode);
		values.put(headofficeaddress, HeadOfficeAddress);
		values.put(phonenumbers, PhoneNumbers);
		values.put(website, WebSite);
		values.put(popularity, Popularity);
		values.put(keywords, Keywords);

		DBHelper.getDB().insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
	}
...

При таком раскладе я получал сохранение объекта в БД за 40-50мс. В итоге все сохранение выливалось мне минуты в 4 минимум. Естественно, что пользователей это не устроило. И я начал курить поисковики и просторы интернета на предмет решения по ускорению моих инсертов. Ответ был найден тут и в документации.

Для ускорения процесса были предприняты следующие шаги:

  • вместо ContentValues использовать InsertHelper
  • перед запуском большого инсерта выключить в БД синхронайз, лок и поместить все в одну транзакцию
  • для корректной работы всего вышеперечисленного необходимо в метод save() каждого объекта передавать объект БД, вытащенный в асинктаске, иначе получим ексепшн

Учитывая, что код гораздо красноречивее, в финале конструкция выглядит так:

public class DBHelper extends SQLiteOpenHelper {
...
private static DBHelper instance;
private static SQLiteDatabase db;

	public static DBHelper getInstance() {
		if (instance == null)
			instance = new DBHelper(Pleazzme.getAppContext());
		return instance;
	}

	public static SQLiteDatabase getDB() {
		if (db == null)
			db = getInstance().getWritableDatabase();
		return db;
	}
...
/*new code*/
    public static void bigDataBegin(SQLiteDatabase _db){
		//_db.setLockingEnabled(false);
		//_db.execSQL("PRAGMA synchronous=0");
		_db.beginTransaction();
	}
	
	public static void bigDataEnd(SQLiteDatabase _db){
		//_db.setLockingEnabled(true);
		//_db.execSQL("PRAGMA synchronous=1");
		_db.setTransactionSuccessful();
		_db.endTransaction();
	}
}

Класс сохраняемого объекта:

...
private static InsertHelper ih;

public void save(final SQLiteDatabase db) {
		if(ih == null)
			ih = new InsertHelper(db, table);
		ih.prepareForInsert();

		try {
			ih.bind(ih.getColumnIndex(key), value);
		} catch (NullPointerException e) {
		}
        .....
		
		ih.execute();
		ih.close();
}
...

И чтобы все это добро «взлетело» код для сохранения объектов:

SQLiteDatabase database = DBHelper.getDB();
DBHelper.bigDataBegin(database);
for (int i = 0; i < currSize; i++) {
        gson.fromJson(o, Data.class).save(database);
	}
DBHelper.bigDataEnd(database);

После всех манипуляций и измерений скорости результат был следующим:

  • до — 40мс на объект
  • после — 4мс на объект

Надеюсь, информация будет для кого-то полезна. Сразу скажу, в первую очередь она рассчитана на начинающих программистов. Если кто-то может предложить более эффективные варианты, буду только рад.

UPD.
В бессознательном порыве радости я поделился своей находкой, как оказалось слишком рано. После тестов выяснилось, что в случае попытки добавить некоторые данные еще одним параллельным тредом, мы получим айяй, кроме того, некоторые устройства ругаются на попытки использовать прагму напрямую.

Таким образом, осталось только использование одной транзакции( код, дающий ошибки я закомментировал). Именно в таком виде он остается максимально безопасным. Правда при этом теряет всякие претензии на что-то, несущее больше информации, чем документация.

Еще одним интересным фактом для меня стало то, что заметный прирост производительности наблюдается на мощных устройствах (SGS2 к примеру с 4.0.4 на борту), при том, что младшие братья (HTC Wildfire) заметного прироста не показали…

Автор: bershadskiy

Поделиться

* - обязательные к заполнению поля