Где хранить данные
файл, SQLite или настоящая база
Что это
Четыре варианта, и выбирают почти всегда между вторым и третьим:
| Где | Когда это правильный ответ |
|---|---|
| в памяти | данные нужны только пока программа работает |
| в файле (JSON, CSV) | настройки, кэш, выгрузка, пара сотен записей |
| SQLite | один процесс, до сотен тысяч записей, нужен поиск |
| сервер БД | несколько процессов, сеть, права доступа, рост |
Решение принимается в первый день, в одну строку, и почти никогда не пересматривается — потому что к моменту, когда оно начинает мешать, на нём уже держится весь проект. Поэтому стоит знать не только, чем варианты отличаются, но и когда именно разница становится заметной.
Как выглядит в коде
Файл против SQLite: цифры
Самый частый сценарий: «пока сложим в JSON, потом переделаем». Хранить список в файле — значит при каждой записи прочитать файл целиком, дописать и записать целиком обратно.
with open("data.json", encoding="utf-8") as f:
rows = json.load(f)
rows.append(record)
with open("data.json", "w", encoding="utf-8") as f:
json.dump(rows, f)Вот сколько времени занимает добавление записей — замерено, одна и та же машина:
| Записей | JSON, с | SQLite, с |
|---|---|---|
| 500 | 1,0 | 0,8 |
| 1 000 | 3,2 | 1,3 |
| 2 000 | 11,1 | 2,6 |
| 4 000 | 45,9 | 5,4 |
| 8 000 | 176,4 | 10,2 |
Смотреть надо не на разницу в последней строке, а на то, как растут столбцы. Удвоение числа записей удваивает время у SQLite и учетверяет у JSON: каждая новая запись заставляет перечитывать всё, что уже накопилось.
И главное: на пятистах записях разницы нет — секунда против восьми десятых. Именно поэтому неправильное решение не видно в тот момент, когда его принимают. Оно становится видно через полгода, когда записей восемь тысяч и программа задумывается на три минуты.
Поиск
поиск одного города среди 50 000
JSON (загрузить файл и перебрать): 50,2 мс
SQLite с индексом : 0,01 мсПять тысяч раз. Причина простая: файл нужно прочитать целиком и разобрать, прежде чем в нём что-то искать, — а база читает только нужную страницу (статья 7.6).
Два процесса пишут одновременно
Это та разница, которую не обойти оптимизацией. Два процесса добавляют по 300 записей в один JSON-файл:
прогон 1: записано 300 из 600 (A=300, B=0)
прогон 2: записано 300 из 600 (A=82, B=218)
прогон 3: записано 300 из 600 (A=11, B=289)Половина данных пропала во всех трёх прогонах, и каждый раз по-разному. Плюс в каждом прогоне один из процессов падал:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)Это чтение файла ровно в тот момент, когда второй процесс успел его очистить, но ещё не записал новое содержимое.
Тот же тест с SQLite:
SQLite — ожидалось 600, записано 600 (A=300, B=300), потеряно 0База умеет то, чего файл не умеет в принципе: выстраивать одновременные записи в очередь. Это и есть главная причина существования баз данных — не скорость, а то, что данные не теряются.
Когда файла достаточно
Файл — отличное хранилище, когда пишет один процесс и редко:
- настройки, которые меняет человек руками;
- кэш ответов внешнего API;
- выгрузка и обмен данными;
- справочник, который обновляется раз в месяц;
- маленький бот, которым пользуешься ты один.
Признак, что файла хватит: ты можешь открыть его в блокноте и это полезно.
Когда SQLite перестаёт хватать
SQLite — не игрушечная база. Это полноценная SQL-база, которой хватает очень надолго, и для одного приложения на одном сервере она чаще всего и есть правильный ответ. Переходить на серверную базу (PostgreSQL, MySQL) имеет смысл, когда появляется хотя бы одно из:
- несколько машин, которым нужны одни и те же данные;
- много одновременных пишущих процессов (SQLite пускает писать по одному);
- права доступа — разным приложениям разный доступ к разным таблицам;
- объём, при котором нужны настройки, резервное копирование по расписанию, репликация.
Обратная сторона: серверная база — это ещё одна программа, которую надо поставить, настроить, обновлять и резервировать. Для бота, которым пользуются десять человек, это работа без выигрыша.
Переход, который делают поздно
Самая дорогая часть перехода с файла на базу — не сама база, а то, что код
обращается к хранилищу из десяти мест. Если с самого начала чтение и запись
собраны в одном файле — storage.py, repository.py, как угодно, — переход
занимает вечер, потому что меняется одна реализация, а не весь проект.
Это, пожалуй, самая полезная архитектурная привычка из всей главы: весь доступ к данным — через одну дверь.
Как сказать ИИ
ИИ по умолчанию предлагает то, что чаще встречалось в обучающих данных, — обычно PostgreSQL и ORM. Для бота на двести строк это лишнее. Поэтому выбор надо называть самому, а у ИИ спрашивать обоснование:
Проект: <описание>. Записей ожидаю <сколько>.
Пишет <один процесс / несколько>.
Данные нужны <только мне / нескольким пользователям>.
Что из этого достаточно: файл, SQLite или сервер БД?
Назови признак, по которому выбранного перестанет хватать.Последняя строчка — самая ценная: она превращает выбор в проверяемое условие.
Сделай весь доступ к данным через один модуль storage.py:
функции add_city, get_city, list_cities, delete_city.
Внутри — SQLite. Остальной код про базу знать не должен.Сейчас данные в JSON-файле, доступ размазан по проекту.
Найди все места, где читается или пишется этот файл.
Список с номерами строк. Код пока не меняй.Перенеси хранение с JSON на SQLite.
Внешние функции storage.py и их сигнатуры не меняй.
Отдельно напиши скрипт переноса уже накопленных данных
и скажи, что делать, если он упадёт на середине.На что смотреть
JSON-файл как основное хранилище у сервера или бота. Это потеря данных, а не медленная работа.
Доступ к данным из десяти мест. Переход на базу превращается в переписывание проекта.
PostgreSQL для проекта на одного пользователя. Настройка, резервные копии и обновления ради нагрузки, которой нет.
Нет проверки, что запись удалась. С файлом ошибка записи часто остаётся незамеченной.
Миграция данных без резервной копии. Скрипт переноса — это первый кандидат на то, чтобы упасть посередине.
Пароли и ключи в той же базе открытым текстом. Хранилище выбрано, а вопрос, что в нём лежит, не задан (статья 5.7).
Нет ответа на вопрос «что будет, если файл откроют дважды». Его стоит задать до того, как ответ придёт в виде пропавших данных.
Шпаргалка
Порядок выбора: память → файл → SQLite → сервер БД.
На 500 записях разницы нет; на 8 000 — 176 секунд против 10.
Время записи в файл растёт квадратично: каждая запись перечитывает всё.
Поиск среди 50 000: 50 мс в файле против 0,01 мс в базе с индексом.
Два процесса на один файл — половина данных теряется, и это измерено.
Файл хорош там, где пишет один процесс и редко.
SQLite хватает надолго; сервер БД — когда машин больше одной.
Весь доступ к данным — через один модуль. Это и есть страховка перехода.