Предлагается к рассмотрению метод динамического определения форматов хранения и типов индексации в таблицах аналитической базы данных Dimension DB. На основе метаданных из источников данных (SQL-запросов, таблиц или представлений) определяется формат хранения или представления данных, а по текущему распределению данных, автоматически определяется тип индексирования.

Как известно, в нагруженных системах стараются разделять системы по профилю нагрузки. Для оперативных систем OLTP контур, для отчетной нагрузки DWH. Реализовано это бывает по разному, все зависит от решаемых задач, текущих ограничений и наличия доступных ресурсов. Обычно нормализованные схемы данных из OLTP систем переносят в хранилища данных в "плоские" таблицы, для ускорения выполнения запросов.
Данная тема достаточно хорошо изучена, в основном на больших объемах стараются разделять транзакционную и отчетную нагрузку. Есть специализированные аналитические базы данных, например ClickHouse. Нередка гибридизация, 2 в 1 – когда классические системы дорабатываются для решения задач ускорения обработки данных: движки для колоночного хранения данных на диске, преобразование табличного формата в колоночный в оперативной памяти, различные виды горизонтального и вертикального масштабирования.
И в классическом подходе и в гибридном варианте обычно проектируют схему данных, наиболее подходящую для быстрой работы запросов, определяют форматы хранения данных, типы индексов. В нашем случае, для локального хранения данных временных рядов разработана Dimension DB, в которой реализован полностью автоматический подход по определению форматов хранения и типов индексации.
Описание системы
Dimension DB — аналитическая база данных блочно-колоночного типа для хранения данных временных рядов. Написана на языке программирования Java. В качестве backend для локального хранения данных используется Berkley DB Java Edition.
Для доступа к данным временных рядов локально, разработано API для записи и чтения данных, которое обращается к локальному хранилищу данных типа «ключ‑значение» Berkley DB. Для внешних систем хранения данных с доступом через SQL интерфейс, доступно только чтение данных. При работе с внешними источниками данных по JDBC используется автоматическая генерация SQL запросов.

Для записи в базу данных Dimension DB поддерживается несколько вариантов API:
Direct — вставка данных в локальную БД с использованием промежуточной Java структуры данных табличного вида;
JDBC — вставка данных в локальную БД с использованием данных, получаемых из внешнего источника данных по JDBC;
JDBC Batch — пакетная загрузка в локальную БД с использованием данных, получаемых из внешнего источника данных по JDBC, подходит для больших объемов данных;
CSV Batch — пакетная загрузка из CSV-файлов, для переноса больших объемов данных из файлов в локальную БД (пока в статусе экспериментальном).
Для чтения данных из Dimension DB используется API, который оптимизирован для соответствующих типов запросов:
Stacked — получение данных на основе функций агрегации, таких как COUNT SUM и AVG, по сути расчет моды по измерению за определенный период времени;
Gantt — сложный расчёт распределения с использованием агрегации COUNT по двум уровням. В этом случае расчитывается мода для анализа зависимостей и связей между двумя выбранными измерениями;
Raw — извлечение исходных данных в табличном формате, для просмотра детальной информации по каждому показателю;
BatchResultSet — извлечение исходных данных в табличном формате с построчным доступом, для получения только части строк из результирующего набора в БД.
Метаданные по наборам данных хранятся локально, используются для запросов к источникам данных по JDBC, CSV-файлам и табличным Java структурам backend-a Berkley DB.
В данный момент реализована поддержка следующих БД:
Таблица № 1. Поддерживаемые БД для работы с API в Dimension DB
№ п.п. | Поддержка API Dimension DB | База данных | Тип базы данных |
1 | Чтение/Запись | Berkley DB | ключ-значение |
2 | Чтение | ClickHouse | аналитическая |
3 | Чтение | Oracle | реляционная |
4 | Чтени�� | PostgreSQL | реляционная |
5 | Чтение | Microsoft SQL Server | реляционная |
6 | Чтение | MySQL | реляционная |
7 | Чтение | CSV файлы | файловая |
Хранение данных
Поддерживаются три формата хранения данных:
RAW — данные загружаются в Berkley DB виде ключа Java-типа int для строковых данных и подходящий Java-тип для остальных. Если тип данных размерности int или меньше — то он сохраняется как есть. Если Long, Float или Double — также как и для строковых значений, сохраняется его ключ в БД в специальной справочной структуре;
ENUM — хранятся в виде ключа Java-типа byte значения. Карта соответствия того или иного значения byte хранится в заголовке блоке. Значения ключа Java-типа int получается аналогично как и при сохранении данных в формате RAW;
HISTOGRAM — сохраняются начальный и конечный индекс повторяющихся данных из диапазона и само значение в виде ключа Java-типа int. Начальный и конечный индекс тоже типа int.
База данных колоночного типа, данные разделены по столбцам таблицы. Внутри колоночного формата, разделение на блоки. Размер блока данных определяется а��томатически по входящему набору данных или вручную, при пакетной вставке через параметр batchSize для JDBC Batch и CSV Batch или в DIRECT режиме регулируя объем входящих данных через промежуточную табличную структуру данных (грузим данные небольшими порциями). Дополнительно есть сжатие блочных структур, реализовано с использованием библиотеки snappy-java. Помогает, если где-то ошиблись с определением типа индексирования в ручном режиме или автоматика не сработала как надо.
Метаданные формата хранения, типа индексации и сжатия хранятся в заголовке блока. Чтение данных работает вне зависимости от выбранного типа индексирования, единообразный способ получения метаданных. Бонус — позволяет переключаться на тот или иной тип индексирования на лету.
Глобальное и локальное индексирование
Привычная работа администратора базы данных — определение типов данных и структур индексирования в БД. Типы данных, индексы. Для определенного профиля нагрузки нужны свои настройки. Потом смотрим нагрузку, делаем корректировки. Если корректировки невозможны, правим статистики по таблицам, при перекосах в данных собираем гистограммы.

Тут практически тоже самое, но в случае глобального индексирования, за типам данных не следим – вся информация подгружается из метаданных. Определяем только тип индексирования — выбираем из трех вариантов: RAW, ENUM или HISTOGRAM.
Если различающихся значений в данных немного и они распределены достаточно равномерно, то есть низкая кардинальность данных и нет значительных повторяющихся значений — выбираем ENUM.
Также, низкая кардинальность, но есть значительные повторяющиеся диапазоны данных. Для этого типа данных больше подходит HISTOGRAM, храним начало и окончание того или иного диапазона.
Для всех остальных случаев, для данных с высокой кардинальностью – выбираем RAW.
Если что-то сделали неправильно, есть опция сжатия — можно сильно не беспокоится об этом.
Следующий этап – передать функции по определению типа индексирования автоматике. Пусть программа сама смотрит за распределением данных и автоматически определяет тип индексирования по блочно. Тут вероятность ошибки снижается кратно – вот они данные, в небольшом наборе – выбираем наиболее подходящий тип индекса.
Сейчас доступны три варианта настройки определения индекса для опции локального индексирования:
ON_LOAD — Данные анализируются при первом получении данных, и на их основе определяется тип индексирования по каждому столбцу таблицы;
FULL_PASS_ONCE — Данные анализируются при первом получении данных, затем при следующих запусках последовательно анализируется каждый столбец таблицы один раз и на основе этой информации определяется тип индексирования;
FULL_PASS_EACH — Тоже же самое что и FULL_PASS_ONCE, только анализируется каждый столбец таблицы при каждом вызове API записи данных. Каждый вызов API - анализируется один столбец и так по кругу.
В первый запуск тип индексирования — RAW, если что‑то поменялось — перестраиваем существующие данные на новый тип индексирования и обновляем статистики, которые хранятся в памяти.
Тестирование производительности
Для тестирования производительности возьмем базу данных заказов такси Нью-Йорка в ClickHouse и загрузим в Dimension DB данные за 2016 год (~ 78 млн. записей). Сравним различные параметры профилей загрузки: время загрузки, объем данных на диске, время на выполнение тестовых запросов к Gantt API для глобального и локального индексирования. Комбинаций будет достаточно много, возьмем для сравнения четыре профиля со сжатием и посмотрим что получается на среднем ПК.
Таблица № 2. Профили загрузки
№ п.п. | TType | IType | AType | Сжатие | Load (min) | Size (GB) |
|---|---|---|---|---|---|---|
1 | TIME_SERIES | GLOBAL | ON_LOAD | true | 99,52 | 9,132 |
2 | TIME_SERIES | LOCAL | ON_LOAD | true | 26,12 | 12,070 |
3 | TIME_SERIES | LOCAL | FULL_PASS_ONCE | true | 26,24 | 11,407 |
4 | TIME_SERIES | LOCAL | FULL_PASS_EACH | true | 26,20 | 11,881 |
Таблица № 3. Тесты производительности gantt API
№ | Test name | № 1 ON_LOAD | № 2 ON_LOAD | № 3 PASS_ONCE | № 4 PASS_EACH |
|---|---|---|---|---|---|
1 | getGanttRawRaw | 14,0 / 10,5 | 15,8 / 10,6 | 17,2 / 10,8 | 16,3 / 10,5 |
2 | getGanttEnumEnum | 4,4 / 2,6 | 11,2 / 8,0 | 11,3 / 8,2 | 10,9 / 7,8 |
3 | getGanttHistHist | 2,3 / 1,2 | 13,6 / 9,8 | 13,9 / 10,2 | 14,3 / 9,7 |
4 | getGanttHistRaw | 9,9 / 7,5 | 12,4 / 9,2 | 12,3 / 9,2 | 12,0 / 8,9 |
5 | getGanttHistEnum | 3,4 / 2,1 | 13,8 / 10,0 | 13,9 / 10,3 | 13,6 / 9,9 |
6 | getGanttEnumRaw | 9,5 / 7,0 | 15,5 / 11,3 | 16,1 / 11,7 | 15,6 / 12,2 |
7 | getGanttEnumHist | 4,2 / 2,7 | 16,5 / 12,7 | 16,5 / 12,6 | 16,5 / 12,1 |
8 | getGanttRawHist | 10,7 / 7,8 | 15,4 / 11,0 | 15,7 / 11,6 | 16,0 / 11,4 |
9 | getGanttRawEnum | 9,6 / 7,0 | 15,4 / 11,3 | 15,6 / 11,2 | 15,4 / 11,1 |
В таблице № 3 первое число — время время исполнения в один поток, вторая — в два потока, в секундах.


Повторить все можно на своем оборудовании, подробности в README и в тестах (см. исходные коды в пакете integration).
Выводы
В целом, подход по автоматическому определению формата хранения данных работает.
На данный момент в наличии прототип системы, который позволяет достаточно комфортно работать с локальными данными порядка десятков миллионов записей при среднем размере строки около 100 байт.
И тут формулировка «комфортно» — требует пояснения, что имеется в виду. Речь про то, чтобы максимально быстро извлечь данные (насколько это позволяет доступный hardware и текущая реализация software) и представить их в удобном для многомерного анализа данных временных рядов виде (с использованием текущей реализации user interface). Например, из последних интересных обновлений Dimension UI, который использует Dimension DB в качестве backend‑a — это реализация фильтрации.
Скринкаст фильтрации в интерфейсе Dashboard в режиме real-time

Скринкаст фильтрации в интерфейсе Dashboard в режиме просмотра истории

Скринкаст фильтрации на Ad-Hoc

Да, Dimension DB медленнее чем ClickHouse. Те же запросы на том же среднем ПК выполняются в пределах 500 миллисекунд. Но есть потенциал по ускорению выполнения запросов, снижению объемов обрабатываемых данных. В текущей реализации достаточно большая избыточность в хранении данных, можно использовать более сжатые структуры, где это позволяют данные. Добавить еще форматов хранения (например битовые карты), что позволит уменьшить занимаемый на диск объем данных и ускорить выполнение запросов.
Исходники проекта
Dimension DB — аналитическая база данных блочно-колоночного типа для хранения данных временных рядов.
Если есть вопросы — можно обратиться к документации по проекту. Подробная инструкция по сборке приложения в README.
Разработано при поддержке «Фонда содействия инновациям».
Вроде все, спасибо за внимание!
