← назад к разделу

«Какая выручка у каждого магазина за январь?» — безобидный вопрос бизнеса, способный положить продакшн-базу. Причина не в размере данных, а в том, что это запрос другого мира: базы и запросы делятся на два больших класса с противоположными паттернами доступа, и то, что хорошо одному, мучительно для другого. Разберём оба мира и мост между ними.

Два паттерна доступа

OLTP (online transaction processing) — мир приложений. Пользователь оформил заказ, открыл профиль, обновил корзину: каждый запрос трогает несколько строк по ключу — нашли по индексу, прочитали или обновили, ответили за миллисекунды. Данные — актуальное состояние мира. Именно под это заточены B-деревья и вся дисциплина транзакций.

OLAP (online analytical processing) — мир аналитики. Вопросы задаёт не пользователь, а аналитик: «сколько бананов продали сверх обычного во время акции?». Такой запрос просматривает миллионы строк, но из каждой берёт два-три столбца и сворачивает их в агрегат — сумму, счёт, среднее. Данные — история событий за годы. Узкое место здесь не поиск позиции на диске, а пропускная способность: сколько байт в секунду удаётся прокачать через сканирование.

Один и тот же SQL умеет и то и другое — но подсистемы хранения под этими запросами нужны разные.

Склад данных и ETL

Пускать аналитиков в продакшн-базу — плохая идея по обе стороны: их запросы-сканы отъедают ресурсы у пользовательских транзакций, а десятки OLTP-систем компании (сайт, склад, доставка, CRM) всё равно не опросишь одним запросом. Поэтому аналитика живёт в отдельной базе — складе данных (data warehouse): read-only копии данных из всех OLTP-систем, собранной процессом ETL (extract — transform — load): извлечь из источников, привести к аналитической схеме, загрузить в склад — периодическими выгрузками или непрерывным потоком событий.

Небольшим компаниям склад не нужен — их данные помещаются в обычный PostgreSQL или вообще в электронную таблицу. Склад появляется тогда, когда OLTP-систем становится много, а истории — терабайты.

Схема «звезда»: факты и измерения

В OLTP-мире схемы разнообразны; в аналитике почти все склады устроены одинаково — по схеме «звезда». В центре — таблица фактов: строка на событие («покупатель купил товар в такой-то момент»), сотни столбцов, миллиарды строк. Вокруг — таблицы измерений: товар, магазин, покупатель, дата, акция. Факт ссылается на измерения внешними ключами; измерения отвечают на вопросы «кто, что, где, когда, как и почему» события.

Даже дата — отдельное измерение: строка на календарный день с атрибутами «праздник/будни», иначе не спросишь «как продажи в выходные против будней». Вариант с донормализованными измерениями (категория товара — отдельной таблицей) называется «снежинкой», но на практике чаще побеждает более денормализованная звезда — с ней проще работать аналитику.

Столбцовое хранение: почему аналитика летает

OLTP-базы хранят данные построчно: вся строка — рядом на диске, что идеально для «прочитай заказ целиком». Но аналитический запрос из сотни столбцов таблицы фактов трогает три — а построчная база всё равно поднимет с диска строки целиком.

Столбцовое хранилище переворачивает раскладку: каждый столбец — отдельный файл, строки восстанавливаются по позиции (k-е значение каждого файла — одна строка). Запрос читает только нужные столбцы — уже выигрыш на порядки. Дальше включается второй эффект: значения одного столбца похожи друг на друга (в столбце «товар» миллиарды строк, но десять тысяч уникальных значений), поэтому столбцы отлично сжимаются — битовыми картами по уникальным значениям, кодированием длин серий. Столбец на миллиарды строк ужимается до мегабайт, фильтры превращаются в побитовые операции, и сканирование упирается уже не в диск, а в кеш процессора. Именно так устроены ClickHouse, Vertica, Redshift и формат Parquet, и поэтому ClickHouse не замена PostgreSQL, а инструмент другого мира.

Запись при этом дороже: вставить строку «в середину» сжатых сортированных столбцов нельзя — и столбцовые базы принимают записи через LSM-подход: накапливают в памяти и сбрасывают пачками.

Материализованные сводки

Если тысяча запросов в день считает SUM(net_price) по одним и тем же осям, честное сканирование каждый раз — расточительство. Ответы кешируют материализованным представлением — таблицей с заранее вычисленным результатом запроса (в PostgreSQL это materialized view). Крайняя форма — OLAP-куб: сетка агрегатов по комбинациям измерений (дата × товар × магазин), где итог за вчера — одно чтение ячейки вместо миллионов строк.

Цена — гибкость: в кубе нельзя спросить то, чего нет среди его измерений («доля продаж товаров дешевле 100 ₽» — если цена не измерение). Поэтому склады хранят сырые события, а сводки держат как ускоритель для типовых запросов.

Где это применяется

Развилка «OLTP или OLAP» всплывает раньше, чем кажется: первый же дашборд «продажи по дням» — уже аналитический запрос, и вопрос «гонять его по продакшн-базе или заводить отдельное хранилище» — это вопрос этой статьи. Ответ по шагам: пока данных немного — читающая реплика PostgreSQL и материализованные представления; когда история разрослась и запросы-сканы стали нормой жизни — отдельное столбцовое хранилище и поток событий в него.

Где спотыкаются начинающие:

  • Гоняют аналитику по продакшн-базе. Первый тяжёлый скан в час пик — и время ответа пользовательских запросов уезжает в хвост.
  • Строят «универсальную» схему. Нормализованная OLTP-схема неудобна аналитику, звезда — приложению. Это два разных представления одних данных, и это нормально.
  • Берут ClickHouse под точечные чтения («найди заказ по id») — столбцовое хранилище на таком паттерне проигрывает PostgreSQL вчистую, как и наоборот.
  • Прячут все ответы в кубы. Сводка без сырых событий — тупик: первый же новый вопрос бизнеса потребует измерения, которого нет.

Что почитать дальше: B-деревья и LSM — движки под обоими мирами; PostgreSQL или ClickHouse — практическая развилка; моделирование в ClickHouse и материализованные представления PostgreSQL — оба мира на практике. Первоисточник — Мартин Клеппман, «Высоконагруженные приложения», глава 3.