Оптимизация работы с базой данных мобильного приложения
CoreData на iOS умеет быть катастрофически медленным, если использовать viewContext для тяжёлых выборок прямо в viewDidLoad. Запрос NSFetchRequest без fetchBatchSize на таблице из 10 000 строк загружает все объекты в память сразу — и это на main thread. На iPad с большой базой данных это 400–600 мс блокировки UI при каждом открытии экрана.
Типичные узкие места
Запросы без индексов. SQLite под капотом у CoreData, Room и большинства мобильных ORM. WHERE по неиндексированному полю на таблице с 50 000 строк делает full scan. На Android Room — добавляем @Index к сущности, на iOS CoreData — выставляем indexed в Data Model Inspector. Разница в скорости выборки — десятки раз.
N+1 запросы. Загружаем список заказов, потом для каждого — отдельный запрос за пользователем. 100 заказов = 101 запрос к SQLite. CoreData решает через relationshipKeyPathsForPrefetching, Room — через @Relation с @Transaction. Flutter + sqflite — JOIN-запрос вместо вложенного цикла.
Запись на main thread. managedObjectContext.save() в iOS, database.insert() в Android — всё это не должно происходить на main thread при больших объёмах. Один save() на 500 объектов на старом iPhone 8 — легко 200–300 мс блокировки.
Решения по платформам
iOS — CoreData
NSPersistentContainer даёт newBackgroundContext() для фоновых операций. Правильная схема:
container.performBackgroundTask { context in
// массовые операции здесь
try? context.save()
DispatchQueue.main.async {
// обновление UI
}
}
NSFetchRequest.fetchBatchSize = 20 — CoreData загружает данные порциями по мере обращения, а не всё сразу. NSFetchedResultsController с sectionNameKeyPath для таблиц с секциями — правильный паттерн, который автоматически обновляет UITableView при изменении данных.
Для bulk insert NSBatchInsertRequest (iOS 13+) работает напрямую в SQLite без создания managed objects — в 10–20 раз быстрее стандартного insert для тысяч записей.
Android — Room
@Query с EXPLAIN QUERY PLAN через adb shell — быстрый способ увидеть, есть ли full scan. Room @TypeConverter для JSON-полей через Gson / Moshi работает, но тормозит на массовых выборках — нормализуйте данные.
Flow<List<Entity>> из Room автоматически эмитит новые данные при изменении таблицы — не нужно вручную инвалидировать кэш. distinctUntilChanged() предотвращает лишние эмиссии если данные не изменились.
Room.databaseBuilder().setQueryCoroutineContext(Dispatchers.IO) — явно указываем, что Room-запросы идут на IO-диспетчере.
Flutter — sqflite / Drift (Moor)
Drift (бывший Moor) — предпочтительный выбор для сложных схем: типобезопасные запросы, миграции, генерация кода. database.transaction() для батч-операций — в транзакции 1000 INSERT выполняются за 50–100 мс, без транзакции — 5–10 секунд (каждый INSERT открывает/закрывает транзакцию SQLite).
Кейс: поиск по 200 000 записей
Приложение для offline-каталога товаров: поиск по названию на Room без FTS занимал 1.8 секунды. Подключили FTS4:
@Fts4
@Entity(tableName = "products_fts")
data class ProductFts(val name: String, val description: String)
MATCH по FTS-таблице — 40–60 мс на том же датасете. Разница ощутима.
Сроки
Аудит и оптимизация запросов — 2–4 дня. Добавление индексов, переход на batch-операции и фоновые контексты — 3–7 дней в зависимости от объёма кодовой базы.







