Чому N+1 запити вбивають продуктивність?
Ми часто стикаємося з ситуацією, коли завантажується список замовлень, а потім для кожного виконується окремий запит за користувачем. 100 замовлень = 101 запит до SQLite. Це класичний N+1 запит. При 1000 записах це 1001 запит, кожен з яких може займати 5–10 мс, підсумок — 5–10 секунд блокування UI. CoreData вирішує через relationshipKeyPathsForPrefetching, Room — через @Relation з @Transaction. Flutter + sqflite — JOIN-запит замість вкладеного циклу. Типова помилка: розробники не дивляться на кількість запитів у лозі. Результат — додаток гальмує, а проблема в кількох рядках коду.
Як індекси прискорюють запити в 10 разів?
SQLite під капотом у CoreData, Room та більшості мобільних ORM. WHERE по неіндексованому полю на таблиці з 50 000 рядків робить full scan. На Android Room додаємо @Index до сутності, на iOS CoreData — виставляємо indexed в Data Model Inspector. Різниця у швидкості вибірки — десятки разів. Без індексу запит може тривати 500 мс, з індексом — 5 мс. Типовий приклад: пошук товару за назвою на таблиці з 200 000 рядків без індексу займає 1.8 секунди, з індексом — 40 мс. Завжди додавайте індекси на поля, що беруть участь у WHERE, JOIN та ORDER BY.
Рішення по платформах
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 — Drift
Drift (колишній Moor) — кращий вибір для складних схем: типобезпечні запити, міграції, генерація коду. database.transaction() для batch-операцій — в транзакції 1000 INSERT виконуються за 50–100 мс, без транзакції — 5–10 секунд (кожен INSERT відкриває/закриває транзакцію SQLite).
Як FTS допомагає при пошуку по 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 мс на тому ж датасеті. Як зазначають розробники SQLite, FTS4 забезпечує повнотекстовий пошук за мілісекунди. Для iOS CoreData використовуйте NSPredicate з MATCH через SQLite безпосередньо, якщо потрібен швидкий повнотекстовий пошук.
Як правильно оптимізувати базу даних мобільного додатку?
Крок 1: Аудит схеми
Виявіть таблиці без індексів та проблемні запити за допомогою EXPLAIN QUERY PLAN.
Крок 2: Додавання індексів
На поля, що беруть участь у WHERE, JOIN та ORDER BY, створіть індекси.
Крок 3: Усунення N+1 запитів
Використовуйте prefetching в CoreData або JOIN в Room/Drift.
Крок 4: Batch-операції
Для масових вставок застосовуйте NSBatchInsertRequest, @Insert з анотацією в Room або транзакції в Drift.
Крок 5: Тестування продуктивності
Виміряйте час запитів до і після, переконайтеся у відсутності блокувань UI.
Порівняння продуктивності до і після оптимізації
| Запит | До оптимізації | Після оптимізації |
|---|---|---|
| Пошук за назвою (200k записів) | 1.8 с (LIKE) | 40 мс (FTS4) |
| Завантаження списку замовлень з користувачами (N+1) | 1.1 с (101 запит) | 20 мс (1 JOIN) |
| Масова вставка 10 000 записів | 15 с (по одній) | 600 мс (batch) |
Приклад коду для batch-вставки в Drift
await database.transaction(() async { for (final item in items) { await database.insert(item); } }); Що входить в оптимізацію?
| Етап | Що робимо | Тривалість |
|---|---|---|
| Аудит схеми | Аналіз таблиць, індексів, запитів | 1-2 дні |
| Оптимізація | Індекси, batch-операції, фонові контексти | 3-7 днів |
| Тестування | Перевірка продуктивності та регресії | 1-2 дні |
| Документація | Рекомендації з моніторингу | включено |
Наш досвід — 5 років на ринку мобільної розробки, понад 50 проєктів з оптимізацією БД. Ми гарантуємо вимірний приріст продуктивності. Зв'яжіться з нами, щоб отримати консультацію та розрахунок вартості. Замовте аудит вже сьогодні та отримайте детальний звіт з рекомендаціями.







