Сравнение СУБД для временных рядов: MinIO, MongoDB и PostgreSQL на 260 миллионах записей
Выбор архитектуры базы данных для хранения и обработки больших объемов исторических финансовых данных — классическая задача бэкенд-разработки. Когда вы строите симулятор (эмулятор) для тестирования торговых стратегий (бэктестинга), производительность хранилища напрямую определяет скорость работы аналитиков.
В данном материале рассматривается практический опыт бэкенд-разработчика, который провел сравнительный анализ трех различных технологических стеков — объектного хранилища MinIO, документарной базы данных MongoDB и реляционной СУБД PostgreSQL (с балансировщиком Pgpool-II) — для работы с массивом исторических данных объемом 262,8 миллиона записей. Этот объем сформирован из 25 лет поминутной истории котировок (японских свечей) по 20 ценным бумагам.
Профиль нагрузки и требования к системе
Перед началом выбора технологического стека были сформулированы четкие требования к поведению базы данных и профилю нагрузки:
- Иммутабельность (неизменяемость) данных: Историческая свеча за конкретную минуту в прошлом никогда не изменяется. Это означает, что для записи требуется только простая операция ленивой вставки отсутствующих данных (идемпотентный upsert).
- Доминирование операций чтения: Запуск торговой стратегии — это на 99% параллельное чтение данных. Система должна выполнять миллионы точечных выборок свечей по составному ключу (биржа, символ акции, интервал времени, временная метка timestamp).
- Строгий контракт видимости (Read-After-Write): Система должна гарантировать, что как только функция записи возвращает управление, последующая функция чтения обязана увидеть эти данные. Это критично при конкурентной записи от разных источников данных.
- Ленивая загрузка (Lazy Loading): Избегать единовременного скачивания сотен миллионов строк в память приложения, обеспечивая быструю потоковую выборку фрагментами.
Подход 1. MinIO: объектное хранилище без изменения схемы
Первой архитектурной гипотезой разработчика была попытка отказаться от традиционных СУБД и использовать S3-совместимое объектное хранилище MinIO. Идея состояла в том, чтобы хранить каждую свечу как отдельный JSON-документ, где имя объекта (ключ) является детерминированной функцией от контекста:
backtest-kit/candle-items/ccxt_binance/TRXUSDT/1m/1767251220000
Этот подход бесплатно решал контракт видимости записей: операция upsert превращалась в один идемпотентный сетевой запрос PUT. Запись выполнялась без предварительного чтения тела объекта.
Почему объектное хранилище не подошло?
Ключевая проблема MinIO и объектных хранилищ в целом — полное отсутствие встроенной поддержки диапазонных запросов (невозможно сделать выборку BETWEEN по временной метке). Чтобы обойти это ограничение, разработчику пришлось внедрить внешний индекс поверх СУБД Redis, который отслеживал диапазоны записанных свечей.
Но главным фактором провала стал физический способ хранения данных в MinIO. Для каждого микро-объекта (размером в сотни байт) MinIO создает на жестком диске отдельный файл данных и отдельный файл с метаданными. При работе с сотнями миллионов свечей файловая система операционной системы перегружается миллионами мелких файлов, что приводит к критическому падению скорости операций ввода-вывода (I/O). В результате бэктест на месяц истории выполнялся слишком медленно.
Подход 2. MongoDB: документарная модель и облачный тупик
Второй попыткой стало использование документоориентированной СУБД MongoDB. Данные хранились в их естественном JSON-подобном виде. Для выполнения идемпотентной вставки использовался метод findOneAndUpdate с оператором $setOnInsert и флагом upsert: true. Для ускорения работы был создан составной индекс.
С точки зрения удобства разработки, экосистема MongoDB показала себя отлично. В отличие от Postgres, для которого часто приходится разворачивать веб-интерфейс управления pgAdmin, разработчики могли использовать удобную десктопную утилиту MongoDB Compass.
В чем MongoDB уперлась в потолок?
Среднее время чтения одной минутной свечи из MongoDB составило около 40 миллисекунд. На первый взгляд, это немного. Однако при запуске бэктеста за 31 день система выполняет последовательно и параллельно миллионы чтений. Математика проста: 31 день × 1440 минут в сутках × 40 мс = около 30 минут реального времени ожидания на один запуск симуляции стратегии. Это сломало рабочий процесс аналитиков, превращая любое изменение параметров алгоритма в получасовое ожидание.
Кроме того, MongoDB несет в себе риски жесткой привязки к инфраструктуре вендора (vendor lock-in) на уровне облачных решений MongoDB Atlas, поскольку на локальном рынке трудно найти специалистов для ее глубокого администрирования и масштабирования.
Подход 3. PostgreSQL + Pgpool-II: победное решение
Финальным и наиболее успешным вариантом стала классическая реляционная база данных PostgreSQL, дополненная балансировщиком соединений и распределителем запросов Pgpool-II.
Топология кластера
Для обеспечения максимальной пропускной способности чтения была развернута топология, состоящая из одной пишущей мастер-ноды (primary) и четырех читающих реплик (read replicas). Балансировщик Pgpool-II берет на себя задачу прозрачной маршрутизации: все точечные запросы чтения SELECT распределяются параллельно по четырем репликам, а команды записи (DML) отправляются строго на мастер-ноду.
Оптимизация записи
Запись свечей в Postgres выполняется через запрос с обработкой конфликтов (upsert):
INSERT INTO ... ON CONFLICT (...) DO UPDATE ... RETURNING *
Результаты вставки сразу же дублируются в быстрый оперативный кэш.
Емкость и масштабирование
Со всеми созданными составными индексами 130 миллионов свечей заняли на диске всего около 34 гигабайт (примерно 260 байт на одну запись). Для Postgres это крайне скромный объем, который даже не требует настройки секционирования (партиционирования) таблиц, поскольку ограничение размера одной таблицы в PostgreSQL составляет 32 терабайта.
Результаты производительности
Время чтения одной свечи сократилось до 10 миллисекунд. Полный цикл тестирования стратегии за месяц сократился с 30 до 7 минут, что позволило разработчикам тестировать гипотезы прямо на рабочем месте, не покидая компьютер. Чтение данных масштабируется линейно при добавлении новых реплик.
Роль Redis Lookup: почему O(1) на самом деле разгружает CPU базы данных
Во всех трех вариантах архитектуры перед основным хранилищем стоял кэш Redis. В среде разработчиков популярно объяснение, что Redis ускоряет поиск за счет асимптотики O(1) по сравнению с логарифмической сложностью O(log n) индексов баз данных (B-Tree). Однако на практике это объяснение ошибочно.
При наличии миллиарда строк высота B-Tree составляет всего 4–5 страниц. Эти страницы почти всегда целиком сидят в оперативной памяти базы данных (Buffer Pool). Поиск по индексу в памяти занимает микросекунды, а сетевой раунд-трип до СУБД обходится в миллисекунды. Таким образом, кэширование не ускоряет время одного конкретного запроса. Настоящий выигрыш Redis дает в кардинальном снижении нагрузки на центральный процессор (CPU) базы данных:
- Для PostgreSQL: Точечный запрос
SELECTтребует от СУБД разбора SQL-синтаксиса, работы планировщика, пингования буферных страниц и блокировок. Это сильно нагружает шину памяти процессора. Redis по короткому ключу отдает идентификатор записи за одну-две кэш-линии процессора. Кэширование позволяет разгрузить CPU мастер-базы, высвобождая ресурсы для реальной работы. - Для MongoDB: Промах по составному индексу заставляет СУБД считывать и декомпрессировать целый блок файлового движка WiredTiger. Кэш Redis (запрос по связке контекст →
_id) избавляет MongoDB от необходимости тяжелой декомпрессии данных в памяти. - Для MinIO: Кэш Redis избавляет систему от выполнения тяжелых запросов сканирования метакаталогов (S3
LIST), заменяя их быстрой выгрузкой плоского списка ключей из оперативной памяти.
Использование гибридной схемы «кэширующий слой Redis + кластер PostgreSQL с read-репликами через Pgpool-II» позволило создать отказоустойчивое, масштабируемое и дешевое в администрировании хранилище для миллионов финансовых котировок.

![Node.JS [ru]](/api/digests/it_development/daily/20260717/assets/sources/we-use-js.jpg)