Программирование с ИИ
ENRU
13.3

Где хранить данные

файл, 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, с
5001,00,8
1 0003,21,3
2 00011,12,6
4 00045,95,4
8 000176,410,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 хватает надолго; сервер БД — когда машин больше одной.

  • Весь доступ к данным — через один модуль. Это и есть страховка перехода.