SQLite — це еквівалент добре зробленого кишенькового ножа для баз даних. Він компактний, гострий і якимось чином завжди під рукою.
А потім одного дня ви намагаєтеся з його допомогою збудувати будинок, і ручка починає боліти.
Саме в цей момент команди починають сперечатися в Slack: «SQLite підходить». «Ні, це вузьке місце». «Нам просто потрібні індекси».
Тим часом користувачі бачать крутилки, а ваш on-call телефон починає нагріватися.
Справжня різниця: файлова база проти серверної бази (і чому це важливо)
SQLite і MySQL обидва розуміють SQL. Ця спільна мова вводить в оману й змушує думати, що вони взаємозамінні.
Це не так. Перша відмінність — не синтаксис, не функції й навіть не швидкість. Це архітектура.
SQLite: бібліотека з файлом
SQLite — це вбудований движок бази даних. Він живе всередині вашого процесу і зберігає дані в одному файлі (плюс допоміжні файли на кшталт
-wal і -shm при використанні WAL). Немає серверного процесу для підключення. Ваш додаток читає й записує
байти через бібліотеку SQLite безпосередньо, використовуючи файлові семантики як межу надійності.
Саме тому SQLite легко доставляти, легко тестувати і він дивно швидкий на одній машині при простій конкуренції.
Це також означає, що файловий менеджер і підсистема зберігання стають частиною історії коректності бази даних — подобається вам це чи ні.
MySQL: окрема служба з конкуренцією на рівні процесів
MySQL — це сервер. Це окремий процес зі своїм управлінням пам’яттю, пулом потоків, внутрішніми замками, журналами redo, буферним пулом,
можливостями реплікації та мережею протоколу. Ваш додаток надсилає запити; MySQL планує їх, координує конкуренцію і зберігає
зміни через свій механізм збереження (зазвичай InnoDB).
Такий поділ дає чисту операційну поверхню: ви можете моніторити, налаштовувати, реплікувати, робити бекапи без зупинки сервісу та
ізолювати навантаження бази від навантаження додатка. Це також додає оперативних обов’язків. Вітаємо, тепер у вас є сервіс бази даних.
Що це означає в продакшні
Якщо ваш додаток — один процес на одній машині з невеликою конкуренцією записів, SQLite може бути чудовим вибором.
Якщо у вас декілька інстансів додатка, черга записів, суворі очікування надійності або амбіції щодо високої доступності, фраза SQLite «це просто файл»
перетворюється на «це просто файл… спільний у розподіленій системі», і ця фраза закінчується звітом про інцидент.
Рішення не в тому, що «SQLite — іграшка, MySQL — серйозне». Рішення в тому, чи краще ваші режими відмов обробляються
файловою блокуванням і поведінкою ОС (SQLite) або спеціалізованим шаром конкурентності й надійності з операційними інструментами (MySQL).
Парафраз ідеї від Werner Vogels (CTO Amazon): краще планувати відмови, ніж робити вигляд, що їх не буде. Саме в базах даних цей підхід окупається.
Факти й історія, що пояснюють сучасні режими відмов
- SQLite створено у 2000 році Д. Річардом Гіппом, спочатку для внутрішніх інструментів. Його задумували вбудованим і простим у розгортанні.
- «Публідомейн»-подібна ліцензія SQLite — одна з причин його повсюдності: вендорам не потрібні юридичні складнощі для вбудовування.
- SQLite — стандарт у багатьох мобільних стеках, бо він малий, надійний на окремому пристрої й не вимагає серверу.
- Режим WAL (write-ahead logging) у SQLite значно покращив конкурентність, але все ще не дає «багато записувачів» у розумінні серверних БД.
- MySQL почався в середині 1990-х і виріс у хостингу, де багато одночасних клієнтів і довгоживучі сервіси — норма.
- InnoDB став дефолтним механізмом збереження починаючи з MySQL 5.5, бо давав транзакції, блокування на рівні рядка та відновлення після збоїв.
- Реплікація сформувала операційну ідентичність MySQL: асинхронні репліки, масштабування читань і патерни відмови стали стандартними практиками.
- Коректність SQLite залежить від гарантій файлової системи. Більшість локальних ФС підходять; мережеві ФС і «креативні» шари зберігання можуть поводитися дивно.
Ознаки міграції: коли SQLite вже не витримує навантаження
1) Ви бачите «database is locked» під реальним навантаженням
SQLite може обробляти багато читачів, і в WAL-режимі він дозволяє читачеві працювати під час запису. Але конкуренція записів — це обрив.
Один записувач одночасно. Якщо у вашому навантаженні є сплески записів — сесії, події, лічильники, стани вхідної скриньки, черги завдань — хвостова латентність зростатиме.
Симптом: запити не падають одразу; вони зависають. Ваш p95 стає p99. Потім користувачі скаржаться. Потім ви додаєте повторні спроби.
Потім ви підсилюєте ефект thundering herd. Саме час припинити торгуватися з фізикою й почати планувати перехід.
2) Ви масштабували додаток горизонтально, і SQLite став проблемою спільного файлу
Запуск SQLite з кількома інстансами працює тільки коли кожна інстанція має свій власний файл бази (на орендаря, на вузол або на пристрій).
Як тільки ви кладете один файл SQLite на спільне зберігання для кількох інстансів, ви створюєте проблему розподіленого блокування й консистентності.
Навіть якщо «все працює в staging», це може деградувати до штормів блокувань, застарілих читань або ризику корупції в залежності від шару зберігання.
MySQL існує саме для того, щоб вам не доводилося ставити вашу доступність на карту через те, як поводиться ваш NFS у момент втрати пакетів.
3) Вам потрібна висока доступність, а не просто бекапи
Резервні копії — це не висока доступність. SQLite гарний для бекапів, бо артефакт — це файл, але відновлення після відмови — не проблема копіювання файлу.
Якщо бізнес вимагає продовжувати приймати запис під час відмови вузла, вам потрібна реплікація, інструменти виборів/відновлення і набір операційних політик. Це вже сфера MySQL.
4) Вам потрібна передбачувана латентність при змішаних навантаженнях
SQLite може блискавично працювати для точкових читань і маленьких транзакцій. Але коли один запит «розростається» — випадковий повний скан, відсутній індекс,
операція на кшталт vacuum — весь процес може постраждати, бо движок БД сидить всередині нього.
У MySQL база має свою пам’ять і планування. Ви можете ізолювати й налаштувати; убити запит; встановити обмеження на користувача.
Ви можете не допустити, щоб ваш додаток став випадковою жертвою.
5) Ви боретеся з надійністю та семантикою бекапів
Надійність SQLite залежить від коректного використання транзакцій і від того, чи підлегла система зберігання виконує fsync як належить. Багато команд
непомітно ставлять pragmas, які жертвують надійністю заради швидкості. Потім їх дивує, що після крашу зникають останні записи.
Якщо ви вже сперечаєтеся про synchronous налаштування та «наскільки це ризиковано», ви вже платите когнітивну ціну.
MySQL дає галузеві механізми контролю надійності та відпрацьовані патерни бекапів/відновлення.
6) Вам потрібна операційна видимість, яку можна передати на on-call
SQLite не постачається з вбудованою performance schema, логами повільних запитів, статусом реплікації, метриками буферного пулу або стандартизованими командами адміністратора.
Ви можете інструментувати його, але фактично будуєте власний шар операцій для бази даних.
Якщо on-call має відповісти «що зараз робить база даних?», а найкраще, що ви можете запропонувати — «я додам логи і деплою нову версію»,
то це не операційна стратегія. Це надія з додатковими кроками.
7) Ваша модель даних виросла поза рамки «одного файлу» операційно
Однофайлова природа SQLite зручна, доки не стає розгортальною артефактом. Доставляння, міграції, блокування, копіювання та валідація цього файлу
перетворюються на ритуал з високою ставкою. У MySQL міграції схем все ще ризикові, але принаймні це світ із зрілими інструментами,
підходами до online-міграцій і відпрацьованими playbook.
8) Ви будуєте мультиорендарні або контрольовані за доступом потоки даних
У SQLite немає облікових записів користувачів, мережевої автентифікації або тонкої моделі привілеїв, як у серверних БД.
Якщо ваша безпека вимагає реального поділу обов’язків або доступу з аудитом, MySQL — більш природний вибір.
Жарт №1: SQLite — це як дуже ефективний вышибала для маленького клубу — доки ваш додаток не запросить увесь інтернет і не наполягатиме, що це ще «маленька зала».
Швидкий план діагностики: знайти вузьке місце за 15 хвилин
Завдання не в тому, щоб «вирішити, що MySQL краще». Завдання — довести, що саме ламається. Ось порядок, який економить час.
Перш за все: підтвердіть категорію симптома (блокування, IO, CPU або дизайн запитів)
- Перевірте наявність контенції замків: помилки на кшталт
database is locked, таймаути або довгі очікування під час записів. - Перевірте затримку зберігання: підвищений disk await, fsync-стали або насичені IOPS.
- Перевірте CPU: завантаження одного ядра всередині процесу додатка (SQLite працює in-process) або зростання часу виконання запитів разом із CPU.
- Перевірте плани запитів: повні скани й відсутні індекси. SQLite залюбки робитиме неправильно швидко, поки одного разу не перестане.
Друге: відтворіть за допомогою контрольованого бенчмарку
Використайте тест, важкий на записи, і тест, важкий на читання. Міряйте p95/p99 латентність, а не лише пропускну спроможність. SQLite часто виглядає добре, доки хвостова латентність не стане жахливою.
Третє: вирішіть, чи виправлення тактичне чи стратегічне
- Тактичне виправлення: додати індекс, зменшити частоту записів, батчити записи, увімкнути WAL, встановити busy timeout або змінити межі транзакцій.
- Стратегічне виправлення: переходьте на MySQL, коли вам потрібні конкурентні записи, HA, кращий операційний контроль або передбачувана продуктивність при кількох клієнтах.
Практичні завдання: команди, виводи та рішення (12+)
Це «покажіть мені» перевірки. Кожне завдання включає команду, що означає вивід і що робити далі.
Команди припускають Linux-хост із файлом SQLite за /var/lib/app/app.db і сервісом MySQL, якщо ви порівнюєте.
Завдання 1: Підтвердьте режим WAL і synchronous налаштування (надійність проти швидкості)
cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA journal_mode; PRAGMA synchronous;'
wal
2
Що це означає: wal покращує конкуренцію читачів та записувачів. synchronous=2 — FULL (безпечніше).
Якщо бачите off або 0, можливо ви жертвуєте стійкістю на користь швидкості.
Рішення: Якщо вам потрібна надійність і встановлено synchronous=OFF, виправте це перш за все. Якщо FULL шкодить продуктивності і записи часті — це сигнал до міграції.
Завдання 2: Перевірте busy timeout (як ви поводитесь під контенцією)
cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA busy_timeout;'
0
Що це означає: 0 означає «падати негайно» при контенції замків (або швидко пробубонити помилки).
Рішення: Встановіть розумний busy timeout у додатку (і, можливо, через PRAGMA), якщо піки блокувань незначні. Якщо щоб пережити навантаження потрібні довгі timeouts, ви маскуєте архітектуру з одним записувачем.
Завдання 3: Швидко ідентифікуйте гарячі таблиці та покриття індексами
cr0x@server:~$ sqlite3 /var/lib/app/app.db ".schema" | sed -n '1,40p'
CREATE TABLE events(
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
payload TEXT NOT NULL
);
CREATE INDEX idx_events_user_created ON events(user_id, created_at);
Що це означає: Ви перевіряєте, чи існують очевидні шляхи доступу. Відсутність композитного індексу — поширена причина раптового уповільнення SQLite.
Рішення: Якщо проблеми продуктивності спричинені відсутніми індексами, часто можна відкласти міграцію. Якщо індексація допомагає читанням, але записи лишаються заблокованими — все одно мігруйте.
Завдання 4: Поясніть план запиту (зловіть повний скан)
cr0x@server:~$ sqlite3 /var/lib/app/app.db "EXPLAIN QUERY PLAN SELECT * FROM events WHERE user_id=42 ORDER BY created_at DESC LIMIT 50;"
QUERY PLAN
`--SEARCH events USING INDEX idx_events_user_created (user_id=?)
Що це означає: Це добре: пошук по індексу, а не повний скан.
Рішення: Якщо ви бачите SCAN TABLE на великій таблиці в продакшн-шляхах — спочатку виправте схему/запит. Не мігруйте лише щоб уникнути додавання індексу.
Завдання 5: Перевірте розмір бази й статистику сторінок (зростання й тиск IO)
cr0x@server:~$ sqlite3 /var/lib/app/app.db 'PRAGMA page_size; PRAGMA page_count;'
4096
258000
Що це означає: Орієнтовний розмір — page_size * page_count (~1.0 GiB тут). Великі БД підвищують штрафи за пропуски кешу та ускладнюють vacuum/обслуговування.
Рішення: Якщо БД швидко росте і ви на одному вузлі — плануйте міграцію раніше, особливо якщо потрібне онлайн-обслуговування.
Завдання 6: Перевірте на роздування WAL (тиск чекпоінтів)
cr0x@server:~$ ls -lh /var/lib/app/app.db*
-rw-r----- 1 app app 1.0G Dec 30 10:12 /var/lib/app/app.db
-rw-r----- 1 app app 6.2G Dec 30 10:12 /var/lib/app/app.db-wal
-rw-r----- 1 app app 32K Dec 30 10:12 /var/lib/app/app.db-shm
Що це означає: Великий WAL-файл зазвичай означає, що чекпоінти не встигають (довгі читачі, невірно налаштовані чекпоінти або сплески записів).
Рішення: Дослідіть довгоживучі транзакції читання. Якщо ви не можете їх контролювати (багато сервісів, фонова робота), міграція — правильне рішення. MySQL краще справляється з таким класом проблем.
Завдання 7: Виміряйте затримку диска (IO — мовчазний вбивця)
cr0x@server:~$ iostat -x 1 3
Linux 6.1.0 (server) 12/30/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.00 0.00 6.00 18.00 0.00 64.00
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 120.0 300.0 6400.0 22000.0 28.5 1.1 95.0
Що це означає: await ~28ms при 95% зайнятості — це суворо. Записи SQLite можуть інтенсивно виконувати fsync залежно від PRAGMA і патернів транзакцій.
Рішення: Якщо диск повільний, виправлення зберігання може вирішити проблеми як для SQLite, так і для MySQL. Якщо у вас вже пристойне сховище і ви все одно бачите контенцію замків — міграція ймовірно необхідна.
Завдання 8: Знайдіть відкриті дескриптори файлів і процеси, що торкаються БД (перевірка реальності multi-writer)
cr0x@server:~$ lsof /var/lib/app/app.db | head
app 2314 app 12u REG 253,0 1073741824 12345 /var/lib/app/app.db
app 2314 app 13u REG 253,0 6657199308 12346 /var/lib/app/app.db-wal
worker 2551 app 10u REG 253,0 1073741824 12345 /var/lib/app/app.db
Що це означає: Кілька процесів використовують один і той же файл БД. Це нормально на одній машині, якщо є координація, але підвищує шанс контенції замків і довгих транзакцій.
Рішення: Якщо архітектура природно передбачає багато записувачів, припиніть намагатися змусити SQLite поводитися як серверна БД.
Завдання 9: Перевірте тип файлової системи й параметри монтування (припущення щодо надійності)
cr0x@server:~$ findmnt -no FSTYPE,OPTIONS /var/lib/app
ext4 rw,relatime,errors=remount-ro
Що це означає: Локальний ext4 зазвичай адекватний. Якщо бачите NFS/CIFS/FUSE-монтування, семантика надійності й блокувань може неприємно здивувати.
Рішення: Якщо файл БД знаходиться на мережевому сховищі і доступність важлива — мігруйте. Не перетворюйте базу даних на експеримент розподіленої файлової системи.
Завдання 10: Спостерігайте очікування замків і таймаути в логах додатка (людський симптом)
cr0x@server:~$ grep -E "database is locked|SQLITE_BUSY|timeout" /var/log/app/app.log | tail -n 5
2025-12-30T10:11:58Z ERROR db write failed: SQLITE_BUSY: database is locked
2025-12-30T10:11:59Z WARN retrying transaction after SQLITE_BUSY
2025-12-30T10:12:02Z ERROR request_id=9f2d api=/events POST latency_ms=4210 sqlite_busy_retries=5
Що це означає: Повторні спроби підвищують латентність. Ви не просто повільні; ви нестабільні під час сплесків.
Рішення: Якщо повтори блокувань корелюють з піками трафіку і записи — ядро продукту, плануйте міграцію. Якщо це рідкісна адміноперація — її можна ізолювати.
Завдання 11: Запустіть короткий мікротест на конкурентність записів (чи колапсує при двох писачах?)
cr0x@server:~$ bash -lc 'for i in {1..2}; do (time sqlite3 /var/lib/app/app.db "BEGIN; INSERT INTO events(user_id,created_at,payload) VALUES(42,datetime(\"now\"),\"x\"); COMMIT;" ) & done; wait'
real 0m0.012s
user 0m0.003s
sys 0m0.002s
real 0m1.104s
user 0m0.004s
sys 0m0.003s
Що це означає: Один запис завершується швидко; інший чекає близько секунди (або більше під навантаженням). Це очікування стає видимою латентністю для користувачів.
Рішення: Якщо в продакшні очікується конкурентність записів — міграція обов’язкова. Якщо записи рідкісні й їх можна батчити — можете залишитися на SQLite.
Завдання 12: Перевірте цілісність SQLite (чи ви вже в проблемі?)
cr0x@server:~$ sqlite3 /var/lib/app/app.db "PRAGMA integrity_check;"
ok
Що це означає: Добре. Якщо повертає щось інше — у вас корупція, і це потрібно розглядати як інцидент у продакшні.
Рішення: Ризик корупції — це каталізатор: пріоритизуйте міграцію і негайно виправляйте налаштування зберігання/транзакцій.
Завдання 13: Порівняйте з MySQL-базою (чи витримає вона конкуренцію?)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_time';"
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| Threads_running | 18 |
+-----------------+-------+
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| Innodb_row_lock_time | 1240 |
+------------------------+-------+
Що це означає: MySQL може мати багато активних потоків. Innodb_row_lock_time дає уявлення про контенцію рядкових блокувань (не досконало, але корисно).
Рішення: Якщо MySQL показує контрольовану row lock time, а SQLite таймить — це ваша обґрунтування для міграції в одному скріншоті.
Завдання 14: Перевірте налаштування надійності MySQL (не мігруйте в новий ризик)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; SHOW VARIABLES LIKE 'sync_binlog';"
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+------------------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog | 1 |
+---------------+-------+
Що це означає: Ці налаштування — дефолтні для багатьох продакшн-систем, які дбають про надійність.
Рішення: Якщо ви мігруєте на MySQL, але ослаблюєте ці параметри без розуміння семантики відмов, ви не підвищуєте безпеку — ви просто змінюєте спосіб втрати даних.
Три корпоративні міні-історії (і потрібний вам висновок)
Міні-історія 1: Інцидент через хибне припущення
Невелика платформа зберігала стан робочих процесів клієнтів у SQLite. Було елегантно: один бінарник, один файл БД, прості бекапи.
Вони навіть використовували WAL і мали «busy timeout», тож відчували себе готовими.
Потім компанія додала другу інстанцію за балансувальником для резервування. Файл БД перемістили на спільний mount, щоб обидві інстанції могли «бачити той самий стан».
Це працювало тиждень, а тиждень — найнебезпечніший проміжок у розробці, бо він дає хибне відчуття правильності.
Під піком навантаження обидві інстанції намагалася записувати. Очікування замків почали накопичуватись. Логіка повторів завзято перезапускала транзакції, що підвищувало швидкість записів,
що збільшувало контенцію замків. Користувачі бачили таймаути, on-call бачив переважно idle CPU. «Але це не обчислення», — казали вони. «Видається нормальним».
Справжній винуватець — припущення: «якщо обидва процеси можуть прочитати файл, вони безпечно координують записи через мережеву файлову систему».
Це пастка. SQLite очікує певних семантик блокувань і fsync. Мережеве сховище може їх іноді забезпечувати — допоки одного разу не перестане.
Виправлення не було героїчним: вони розгорнули MySQL, спрямували обидві інстанції на нього і прибрали шар спільного файлу.
Перший інцидент після цього був нудним. Нудьга — правильний результат.
Міні-історія 2: Оптимізація, що обернулася проти
Інша команда мала сервіс інтенсивного прийому даних на SQLite. Вони бачили сплески латентності. Хтось знайшов блог-пост про PRAGMA для швидкості і застосував зміни:
PRAGMA synchronous=OFF і PRAGMA journal_mode=MEMORY.
Графіки виглядали дивовижно. Менше дискового IO, вища пропускна здатність, менше таймаутів. Команда тихенько святкувала, бо вирішили не святкувати голосно.
Через два місяці відбулося перезавантаження хоста під час запису через рутинний патч ядра. Нічого драматичного. Звичайна операція.
Після перезавантаження БД почала викидати помилки цілісності. Деякі недавні дані зникли, деякі FK-зв’язки виявилися непослідовними.
У команди були бекапи, але відновлення означало втрату легітимних недавніх записів. Вони відновлювали дані з логів upstream і часткових експортів.
Проблема не в тому, що існують налаштування надійності; проблема в тому, що система насправді вимагала «не втрачати прийняті записи».
Якщо ваші вимоги такі — оптимізації, що послаблюють надійність, не є оптимізаціями, це кредити з хижацьким відсотком.
Вони мігрували на MySQL з коректними налаштуваннями redo і binlog sync. Продуктивність була задовільною, а відмови стали відновлюваними, а не фатальними.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Третя організація використовувала SQLite для агента, що запускався у середовищах клієнта. Архітектура була логічною: один агент, локальна БД,
низька конкуренція. Вони все одно ставилися до цього як до продакшн-зберігання, бо клієнти не хвилюються, що це «лише агент».
Вони впровадили три нудні практики: транзакційні записи з чіткими межами, періодичні перевірки цілісності і рутину бекапів, яка копіювала БД
через online backup API SQLite, а не сире копіювання файлу під час активних записів.
У одного клієнта з’явився ненадійний диск. Агент почав логувати помилки IO. Оскільки перевірки цілісності вже були на місці, агент швидко виявив корупцію,
ізолював БД, відновив з останнього працездатного бекапу і прогнав невеликий буфер останніх подій з локальної черги.
Клієнт не подав жодного тікета. Вони всередині команди побачили алерт, відкрили інцидент і закрили його з плечовим shrug.
Цей shrug — звук хорошої операційної практики.
Висновок: ви не мігруєте просто тому, що можете. Ви мігруєте тому, що змінилися ваші операційні вимоги. Поки вимоги не змінилися — робіть нудну роботу з коректності.
Жарт №2: Найпростіший спосіб зробити SQLite «високодоступним» — роздрукувати файл бази і тримати його в двох різних офісах.
Поширені помилки: симптом → корінна причина → виправлення
1) Симптом: випадкові таймаути під час піків трафіку
Корінна причина: контенція записів (обмеження одного записувача), плюс повтори, що підсилюють навантаження.
Виправлення: зменшити частоту записів (батчинг), скоротити транзакції, увімкнути WAL, додати busy timeout з backoff. Якщо багато записувачів — мігруйте на MySQL.
2) Симптом: помилки «database is locked» зʼявилися після додавання фонового воркера
Корінна причина: новий процес додав друге джерело записів; транзакції перекриваються; довгі читачі блокують чекпоінти WAL.
Виправлення: забезпечити серіалізацію записувачів (черга одного записувача) або перенести навантаження записів в MySQL. Аудитуйте фонві роботи на предмет довгих транзакцій читання.
3) Симптом: WAL файл росте безупинно
Корінна причина: чекпоінт не може завершитися, бо довгоживучі читачі утримують старі сторінки; або налаштування чекпоінтів занадто м’які.
Виправлення: усуньте довгі транзакції читання; явно запускайте чекпоінти у вікна низького трафіку; розгляньте міграцію, якщо багато сервісів читають паралельно.
4) Симптом: після крашу/перезавантаження відсутні недавні дані
Корінна причина: ослаблені налаштування надійності (synchronous=OFF, ненадійна ФС, небезпечні параметри монтування) або записи не були обгорнуті в транзакції.
Виправлення: відновіть PRAGMA надійності, використовуйте транзакції, перемістіть БД на надійне локальне сховище. Якщо потрібні сильні гарантії на масштабі — MySQL з правильними flush/binlog параметрами.
5) Симптом: запит швидкий у dev, повільний у prod
Корінна причина: dev-датасет малий; у prod є скію; відсутній індекс; план запиту змінився; важкі LIKE/ORDER BY без підтримки індексів.
Виправлення: запустіть EXPLAIN QUERY PLAN на даних, схожих на продакшн; додайте композитні індекси; перепишіть запити. Не звинувачуйте SQLite за відсутність індексу.
6) Симптом: CPU додатка зростає під час формування звіту
Корінна причина: SQLite працює в процесі додатка; важкі запити відбирають CPU від обробки запитів.
Виправлення: ізолюйте звіти, запускайте їх з репліки (MySQL) або перемістіть аналітику в окреме сховище. Мінімум — запускайте звіти в окремому процесі з обмеженнями ресурсів.
7) Симптом: «працювало, поки ми не контейнеризували»
Корінна причина: файл БД помістили на overlay filesystem або мережевий том; семантика fsync/блокувань змінилася; IO уповільнився.
Виправлення: розмістіть SQLite на локальному персистентному томі з відомою семантикою, або припиніть використовувати SQLite у сценарії з багатьма контейнерами і мігруйте на MySQL.
Чеклісти / покроковий план: мігруйте без героїзму
Чекліст рішення: чи варто мігрувати цього кварталу?
- У вас більше ніж один записувач у продакшні (декілька процесів, воркери, cron, інстанції додатка)?
- Чи хвостова латентність (p95/p99) зумовлена очікуваннями замків або повторними спробами?
- Чи ви вимагаєте високої доступності (приймати записи під час відмови вузла), а не «у нас є бекапи»?
- Чи потрібні вам онлайн-операції: бекапи без даунтайму, зміни схем з мінімальним впливом, вбивання запитів, видимість?
- Чи ви розміщуєте SQLite DB на спільному або мережевому сховищі?
Якщо ви відповіли «так» на два і більше пунктів — плануйте міграцію. Якщо «так» на спільне сховище або вимоги HA — припиняйте дискусію і заплануйте її.
План міграції: розумна послідовність
- Визначте вимоги до коректності: надійність (що можна втратити?), консистентність (read-your-writes?), допустимий час простою.
- Зробіть інвентар відмінностей схеми: типи даних, поведінка autoincrement, зберігання дати/часу, обмеження, значення за замовчуванням.
- Оберіть стратегію міграції:
- Великий відрізок: зупинити записи, експорт/імпорт, переключити. Просто, потребує вікна даунтайму.
- Подвійний запис: писати в обидві, читати зі SQLite, потім переключити читання, потім зупинити SQLite. Складніше, менше даунтайму.
- Схожий на CDC: зазвичай надмірно для SQLite, якщо тільки у вас вже немає журналу подій.
- Розгорніть MySQL з продакшн-дефолтами: бекапи, моніторинг, лог повільних запитів, розумні налаштування надійності.
- Заповніть дані з SQLite в MySQL і валідуйте підрахунки й контрольні суми.
- Запустіть canary-чтення: порівняйте результати запитів між SQLite і MySQL для критичних шляхів.
- Поступово переключайте трафік, якщо можливо; якщо ні — робіть чистий cutover з планом відкату.
- Тримайте SQLite у режимі лише для читання певний період як страховку, потім архівуйте.
Операційний чекліст для MySQL (щоб ви не «оновилися» в хаос)
- Бекапи протестовано відновленням (не лише «задачі бекапу зелені»).
- Моніторинг реплікаційної затримки (якщо є репліки), вільного місця, hit rate буферного пулу, повільних запитів, насичення підключень.
- Пулінг підключень у додатку. Не відкривайте 2 000 з’єднань до MySQL, бо помітили потоки.
- План міграцій схеми (online де можливо або заплановані вікна).
- План ємності для зростання сховища (InnoDB росте і любить вільне місце для обслуговування).
Часті запитання
1) Чи SQLite «не для продакшн»?
Воно цілком підходить для продакшну — коли «продакшн» означає вбудованість, одиночний вузол і низьку конкурентність записів. Він використовується в продакшні на мільярдах пристроїв.
Неправильний крок — використовувати його як спільну мульти-писальну базу для горизонтально масштабованого сервісу.
2) Чи робить WAL режим SQLite здатним обробляти багато писачів?
WAL допомагає читачам не блокувати писачів і навпаки, але не перетворює SQLite на систему з багатьма записувачами. По-прежньому лише один писач одночасно.
WAL — це покращення конкурентності, а не координатор розподілених транзакцій.
3) Який найбільш практичний показник, що варто мігрувати?
Постійна контенція записів під нормальним навантаженням — таймаути, помилки блокувань, повтори, що підвищують латентність. Якщо продукт сильно залежить від записів — це межа.
4) Чи можу я помістити SQLite на NFS, якщо буду обережний?
Можна, і деякі так роблять, і деякі потім отримують виклики під час інцидентів. Семантика блокувань і надійності файлових систем через мережу — це податок на надійність.
Якщо потрібні кілька машин, використовуйте серверну базу даних.
5) Чи MySQL завжди швидший за SQLite?
Ні. SQLite може бути швидшим для простих локальних читань і малих записів, бо немає мережевого відгуку і мінімальні накладні витрати.
MySQL перемагає, коли важливі конкуренція, ізоляція, буферизація й операційні контролі.
6) А як щодо використання SQLite як кешу, а MySQL як джерела істини?
Це може працювати, якщо ви розглядаєте SQLite як тимчасовий і відновлюваний кеш. Момент, коли SQLite стає «єдиним місцем», де щось живе, він перестає бути кешем і стає базою даних.
7) Як не мігрувати занадто рано?
Доведіть, що вузьке місце саме в архітектурі. Якщо проблема — відсутні індекси або некоректні межі транзакцій, виправте це спочатку. Міграція виправдана, коли архітектура обмежує вас, а не коли схема потребує догляду.
8) Який найпростіший безпечний підхід до бекапу SQLite?
Використовуйте online backup API SQLite (через ваші звʼязки мови або CLI backup-функції), а не копіювання файлу під час активних записів.
Перевіряйте бекапи відновленням і запуском PRAGMA integrity_check;.
9) Якщо я мігрую на MySQL, який новий режим відмов вдарить першим?
Шторми підключень і неправильно налаштований пулінг. SQLite приховував це, бо працював у процесі. MySQL охоче прийме ваше навантаження, поки не закінчаться потоки або пам’ять.
10) Використовувати MySQL чи щось інше (Postgres тощо)?
Якщо вибір стоїть саме між SQLite і MySQL, обирайте MySQL коли потрібні серверна конкуренція, реплікація і операційні інструменти.
Якщо ви робите ширшу оцінку — вирішуйте за навичками команди та операційними обмеженнями. Основна ідея: серверна БД — коли потрібні серверні властивості.
Висновок: практичні наступні кроки
SQLite не «ламається». Команди просять від нього роботу, на яку він не був найнятий, а потім звинувачують його в межах.
MySQL не «кращий» абстрактно. Він кращий, коли вам потрібні конкурентні записи, патерни HA, спостежуваність і операційні контроли, які не вимагають винаходу бази даних заново.
Якщо ви не впевнені — не сперечайтеся. Вимірюйте. Пройдіть швидкий план діагностики, виконайте наведені вище завдання і шукайте сигнатуру:
очікування замків, інфляція хвостової латентності і операційні ризики навколо надійності та спільного зберігання.
- Якщо проблема в дизайні запитів: додайте індекси, виправте транзакції, протестуйте знову.
- Якщо проблема в диску: спочатку виправте IO; погані диски роблять будь-яку базу даних некомпетентною.
- Якщо проблема в конкуренції й вимогах HA: заплануйте міграцію на MySQL і ставте її як інфраструктурний проєкт, а не рефакторинг.
Найкращий час мігрувати — до того, як ви будете дебагувати заблоковану базу о 3:00 ранку, намагаючись згадати, чи synchronous=OFF було «тільки тимчасово».