JSON как база данных — плюсы, минусы и когда стоит остановиться

Обновлено:

Можно ли использовать JSON-файл как базу данных? Можно, но только для небольших данных, которые редко меняются и которые пишет один процесс. Как только появляется конкурентная запись, частые обновления или данные начинают расти, JSON-файл становится источником потерь и повреждений. Ниже — где граница и как её не пропустить.

Что такое JSON как база данных

Это паттерн, при котором вместо СУБД вы читаете и пишете структурированные данные в текстовый файл .json. Никаких серверов, подключений и лицензий — просто файл на диске.

{
  "users": [
    { "id": 1, "name": "Иван", "email": "ivan@example.com" },
    { "id": 2, "name": "Мария", "email": "maria@example.com" }
  ],
  "settings": {
    "theme": "dark",
    "language": "ru"
  }
}

Подход встречается повсюду: конфигурации (package.json, tsconfig.json), простые CLI-инструменты, прототипы, фронтенд-моки. Проблемы начинаются, когда стирается граница между «хранить конфиг» и «хранить бизнес-данные».

Преимущества JSON-файла

1. Нулевая настройка

Не нужно ставить PostgreSQL, заводить пользователей и пробрасывать порты в Docker. Удобно для прототипов, хакатонов и MVP за выходные.

2. Человекочитаемость

Открыли в редакторе — увидели все данные, без SQL-клиентов.

3. Поддержка во всех языках

Python, JavaScript, Go, PHP, Ruby — парсинг JSON есть в стандартной библиотеке или в одном пакете.

4. Простой перенос

Скопировали файл — передали коллеге или перенесли на другую машину.

5. Гибкая схема

Новое поле в одной записи ничего не ломает. Удобно, пока структура данных не устоялась.

Недостатки: почему JSON-файл — не настоящая база данных

Нет транзакций

Если процесс упадёт посреди записи, а файл перезаписывается «на месте», он останется обрезанным и не распарсится. В SQLite и PostgreSQL незавершённая транзакция откатывается автоматически. С JSON защититься можно только атомарной записью: писать во временный файл и переименовывать его поверх старого.

import json, os, tempfile

def save_json(path: str, data) -> None:
    directory = os.path.dirname(os.path.abspath(path))
    fd, tmp = tempfile.mkstemp(dir=directory, suffix='.tmp')
    with os.fdopen(fd, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
        f.flush()
        os.fsync(f.fileno())
    os.replace(tmp, path)  # атомарная замена в пределах одной файловой системы

Атомарная запись защищает от обрезанного файла, но не от потерянных обновлений — о них ниже.

Нет параллельного доступа

Два процесса или два запроса одновременно читают файл, меняют каждый своё и записывают — побеждает последний, изменения первого теряются. В веб-приложении с несколькими одновременными пользователями это вопрос времени. Нужны блокировки файла или единственный процесс-писатель, то есть вы начинаете писать свою СУБД.

Производительность падает с ростом данных

Чтобы обновить одну запись, нужно:

  1. прочитать весь файл в память;
  2. найти объект;
  3. изменить его;
  4. перезаписать весь файл.

Стоимость каждой записи растёт вместе с размером файла. Пока файл маленький, это незаметно, но при частых обновлениях и росте данных задержки становятся ощутимыми.

Нет индексов и запросов

В SQL условие WHERE user_id = 42 использует индекс. В JSON-файле приходится перебирать все записи (filter, find) — O(n) на каждый запрос.

Нет разграничения прав

В PostgreSQL можно дать доступ только на чтение отдельных таблиц. JSON-файл доступен целиком или не доступен вовсе.

Бинарные данные

Изображения и файлы приходится кодировать в Base64, что увеличивает их размер примерно на треть и утяжеляет каждую запись файла.

Библиотеки для JSON-хранилища

Если всё же используете JSON-файл, берите библиотеку, а не ручные JSON.parse/JSON.stringify — но проверяйте, что именно она гарантирует.

lowdb (Node.js / TypeScript)

Популярная библиотека для JSON-хранилища в Node.js. Поддерживает TypeScript, адаптеры для файла и памяти; файловый адаптер пишет файл атомарно.

npm install lowdb
import { Low } from 'lowdb'
import { JSONFile } from 'lowdb/node'

type Data = { users: { id: number; name: string }[] }

const adapter = new JSONFile<Data>('db.json')
const db = new Low(adapter, { users: [] })

await db.read()
db.data.users.push({ id: 1, name: 'Иван' })
await db.write()

Подходит для Electron-приложений, CLI-инструментов и небольших локальных утилит. От конкурентной записи из нескольких процессов lowdb не защищает.

TinyDB (Python)

Небольшая документная база для Python с синтаксисом запросов, напоминающим MongoDB. Проект считается завершённым по функциональности и в основном поддерживается, а не развивается.

pip install tinydb
from tinydb import TinyDB, Query

db = TinyDB('db.json')
User = Query()

db.insert({'name': 'Иван', 'role': 'admin'})
result = db.search(User.role == 'admin')
print(result)

Подходит для небольших скриптов и прототипов. Стандартное хранилище TinyDB перезаписывает файл на месте, поэтому параллельная запись из нескольких процессов небезопасна, а для критичных данных стоит подумать о SQLite.

JSON Server (Node.js)

Не база данных, а инструмент для mock REST API поверх JSON-файла: фронтенд можно разрабатывать без реального бэкенда.

npx json-server db.json --port 3001

После запуска доступны эндпоинты вида GET /users, POST /users, PUT /users/1, DELETE /users/1. В старых версиях (0.x) для отслеживания изменений файла нужен флаг --watch.

Подходит для моков API, демо и интеграционных тестов фронтенда.

JSON vs SQLite

Главная альтернатива JSON-файлу — SQLite: тоже без сервера и в одном файле, но с гарантиями настоящей СУБД. Подробнее — в статье что такое SQLite.

| Критерий | JSON-файл | SQLite | |---|---|---| | Установка | Ничего не нужно | Python — стандартная библиотека; Node.js — better-sqlite3 или встроенный node:sqlite; Go — драйвер | | Транзакции | Нет | ACID | | Индексы | Нет | Есть | | Параллельный доступ | Опасен | Много читателей и один писатель (WAL) | | Поиск | O(n) перебор | O(log n) по индексу | | Комфортный объём | Небольшие файлы | От килобайт до очень больших баз | | Человекочитаемость | Да | Нет (бинарный формат) | | Бэкап | Копия файла после атомарной записи | .backup или VACUUM INTO | | Продакшен | Конфиги и небольшие данные | Да: мобильные приложения, браузеры, серверы |

SQLite хранит историю и закладки Firefox, данные множества мобильных приложений и системных сервисов iOS и Android. Это не учебная игрушка, а проверенное решение.

JSON внутри реляционной базы

Современные СУБД хранят JSON как тип данных в обычной таблице. Это снимает главные недостатки JSON-файла и сохраняет гибкость.

PostgreSQL JSONB

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  attributes JSONB
);

INSERT INTO products (name, attributes)
VALUES ('Ноутбук', '{"brand": "Apple", "ram": 16, "tags": ["M3", "MacBook"]}');

-- GIN-индекс ускоряет операторы вхождения вроде @>
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);
SELECT * FROM products WHERE attributes @> '{"brand": "Apple"}';

-- Для частых запросов по одному полю удобнее индекс по выражению
CREATE INDEX idx_products_brand ON products ((attributes->>'brand'));
SELECT * FROM products WHERE attributes->>'brand' = 'Apple';

JSONB — бинарное представление JSON в PostgreSQL: данные индексируются, запросы быстрые, а рядом остаются транзакции и строгие колонки.

MySQL / MariaDB JSON

MySQL с версии 5.7 поддерживает тип JSON с функциями JSON_EXTRACT, JSON_SET и проверкой, что значение — корректный JSON. Индексировать поля внутри JSON можно через генерируемые колонки.

Когда JSON-файл — правильный выбор

  • Конфигурация приложения (настройки, переводы, темы).
  • Seed-данные для тестов.
  • Mock API при фронтенд-разработке.
  • Локальный кэш настроек в CLI-инструменте.
  • Небольшие пользовательские данные в десктопном приложении с одним процессом-писателем.
  • Прототипы, которые точно не пойдут в продакшен.

Когда JSON-файл — антипаттерн

  • Несколько одновременных пользователей или процессов-писателей.
  • Тысячи записей и больше.
  • Нужны фильтрация, сортировка, агрегация.
  • Данные часто обновляются.
  • Это продакшен с бизнес-данными.
  • Нужна история изменений или откат.

Бэкап JSON-файла

JSON-файл уязвим, как любой другой: случайное удаление, перезапись сломанными данными, сбой диска. Минимум — копия с датой в имени и хранение нескольких последних версий:

mkdir -p backups
cp db.json "backups/db_$(date +%Y%m%d_%H%M%S).json"
ls -t backups/db_*.json | tail -n +15 | xargs -r rm -f   # оставить 14 последних

Копируйте файл в момент, когда в него никто не пишет, или после атомарной записи — иначе в копию может попасть обрезанный файл. Перед сохранением полезно проверить, что JSON парсится: python -m json.tool db.json > /dev/null.

Если хочется хранить версии вне машины, JSON-файл можно загружать в dbsend как произвольный файл (-e generic_file): dbsend будет хранить версии и проверять контрольную сумму, но не разбирать содержимое. Для SQLite dbsend показывает таблицы и число строк, а на платных тарифах — diff между версиями; подробнее на странице бэкап SQLite в dbsend.

Миграция с JSON на SQLite

  1. Возьмите встроенный модуль sqlite3 (Python) или better-sqlite3 (Node.js).
  2. Сделайте копию JSON-файла.
  3. Создайте в SQLite таблицы под вашу структуру.
  4. Вставьте записи в одной транзакции — так быстрее и атомарно.
  5. Сравните количество записей до и после.
  6. Замените файловые операции в коде на SQL-запросы.
import json
import sqlite3

with open('db.json', encoding='utf-8') as f:
    data = json.load(f)

conn = sqlite3.connect('app.db')
conn.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')

with conn:  # одна транзакция: либо всё, либо ничего
    conn.executemany(
        'INSERT INTO users (id, name, email) VALUES (?, ?, ?)',
        [(u['id'], u['name'], u.get('email')) for u in data['users']],
    )

count = conn.execute('SELECT COUNT(*) FROM users').fetchone()[0]
print(f"В JSON: {len(data['users'])}, в SQLite: {count}")
conn.close()

После переезда не забудьте про бэкапы самой SQLite-базы — как делать их без повреждения, описано в статье как сделать бэкап SQLite.

Итог

Данные меняются редко и их мало — JSON. Данные меняются часто, их много или пишут несколько процессов — минимум SQLite, а для серверного приложения — PostgreSQL с JSONB для гибких полей.

Частые вопросы

Можно ли использовать JSON-файл в продакшене?

Для конфигурации и небольших редко меняющихся данных — да. Для бизнес-данных приложения — нет: берите SQLite или PostgreSQL.

Чем JSON-файл отличается от SQLite?

SQLite — полноценная реляционная СУБД в одном файле: транзакции, индексы, конкурентное чтение и SQL. JSON — текстовый формат без гарантий целостности; всё, что выше простого чтения и записи, придётся писать самому.

Какие библиотеки работают с JSON как с БД?

Для Node.js — lowdb, для Python — TinyDB или встроенный модуль json с атомарной записью, для моков API — JSON Server. Ни одна из них не решает проблему конкурентной записи из нескольких процессов.

JSONB в PostgreSQL — это то же самое, что JSON-файл?

Нет. JSONB — тип данных внутри PostgreSQL: хранится в бинарном виде, индексируется, поддерживает операторы поиска и участвует в транзакциях.

Что выбрать для прототипа — JSON или SQLite?

Если прототип точно не пойдёт в продакшен — JSON подойдёт. Если есть шанс, что проект вырастет, сразу берите SQLite: в Python модуль встроен, в Node.js есть better-sqlite3 и встроенный модуль node:sqlite в новых версиях (пока экспериментальный).

Как защитить JSON-файл от повреждения?

Пишите атомарно (временный файл + переименование), держите один процесс-писатель и делайте регулярные копии. Если нужны параллельная запись и транзакции — это сигнал переходить на SQLite.