Оптимізація роботи з базою даних мобільної програми
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 разів швидше для тисяч записів.
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 днів залежно від обсягу кодової бази.







