№ 077
09 сентября 2026
8 мин чтения

Иерархический OLTP: измеряем следующий уровень унификации

Выносим разрезы аналитики на верхний уровень транзакции в квартетной модели: 1 млн транзакций, Postgres 16, честные цифры выигрышей и проигрышей.

В статье про масштабирование квартетной модели мы показали, что единая таблица id, up, t, val держит 31 млрд записей и 703 млн банковских транзакций. Там же есть неявное преимущество унификации, о котором стоит сказать вслух: пустые колонки мы не храним вовсе — отсутствующий реквизит просто не порождает строку, тогда как классическая широкая таблица тратит место на NULL в каждой из 50+ колонок.

Следующая идея звучала так: раз структура и так иерархична (up — родитель), почему бы не сделать OLTP с иерархией под разрезы аналитики? Берём список транзакций, выносим справочную часть и даты — филиал, валюту, дату, тип операции — на первый уровень («разрез»), а транзакционные детали оставляем вторым уровнем под ним. Повторяющиеся значения хранятся один раз на группу, агрегирующие выборки ходят по верхнему уровню, а уровней может быть и больше двух — их определяет частотность.

Гипотеза обещала сократить и хранение, и агрегирующие выборки, «ничем не жертвуя». Мы её измерили. Часть подтвердилась, часть — нет, и границы применимости оказались интереснее самой идеи.

Методика

Данные — как в статье про 703 млн: банковская транзакция с 54 реквизитами (PAN, суммы, даты, коды ISO 8583), из которых 45 заполнены. Сгенерирован 1 млн транзакций за 90 дней: 50 филиалов, 3 валюты, 10 MCC-типов, всё со скосами распределений. Postgres 16.4, одна машина (12 логических ядер, 16 ГБ RAM), индексы как в статье: (up, t) и (t, lower(left(val,127))). Замер — тёплый прогон (второй запуск подряд); скрипты генерации и замера открыты, всё воспроизводится.

Сравнивались пять раскладок одних и тех же данных:

РаскладкаЧто этоСтрокРазрезовТранзакций на разрез
wideклассическая широкая таблица, 54 колонки1,0 млн
flatплоские квартеты: транзакция + 45 реквизитов46,0 млн
hier2разрез = дата + филиал44,0 млн4 500222
hier4разрез = дата + филиал + валюта + тип42,5 млн102 4239,8
hier8разрез = те же + ISO + ПК + POS + статус46,5 млн946 1361,1

Результат 1: хранение — выигрыш есть, но скромный, и его легко потерять

flathier2hier4hier8
Строк на транзакцию46,044,042,546,5
Размер с индексами5,9 ГБ5,7 ГБ (−4%)5,5 ГБ (−7%)6,1 ГБ (+3%)

Вынос четырёх полей из 45 экономит 7% — арифметика сходится: экономия ≈ доля вынесенных реквизитов, помноженная на (1 − 1/размер группы). Но hier8 — главный урок таблицы: восемь полей в разрезе дали 946 тысяч групп на миллион транзакций — по 1,1 транзакции на группу. Каждая группа — это её собственные строки, и хранение стало хуже плоского. Правило частотности из исходной идеи — не украшение, а условие выживания схемы: выносить поле в разрез можно только пока произведение кардинальностей разреза остаётся на порядки меньше числа транзакций.

Результат 1а: экономия зависит от профиля записи — на Ф110 это уже 40%

Семь процентов на банковской транзакции — следствие её профиля: у записи 45 заполненных полей, и почти все — высококардинальные детали (PAN, RRN, суммы), в разрез уходит лишь 4 из них. Экономия подчиняется простой формуле: доля вынесенных полей × (1 − 1/размер группы).

Противоположный профиль — иерархия из статьи Neoflex про BI на квинтетах: форма банковской отчётности Ф110. Форма (отчётная дата, точность) → коды расшифровки (код, сумма в рублях, сумма в валюте). У показателя всего три своих поля, а вынесенные дата и точность — это 2 поля из 5. Мы прогнали и этот профиль: 4470 отчётных дат (как в статье Neoflex), 200–600 кодов на форму, 1,79 млн показателей:

плоскоиерархия
Строк на показатель5,03,0 (−40%)
Размер с индексами1174 МБ731 МБ (−38%)
Сумма ₽ по датам, весь корпус (4470 дат)4,3 с7,8 с (проигрыш 1,8×)
Сумма ₽ по датам, один год (~8% корпуса)1,05 с0,47 с (выигрыш 2,2×)
Динамика одного кода2 мс1 мс
Все показатели одной даты2 мс2 мс

Главный выигрыш Ф110-профиля — хранение: минус 40% строк. С агрегатами по мере картина двусторонняя, и она уточняет вывод из транзакционного замера: на селективном диапазоне (год из 12 лет) иерархия быстрее — 365 форм находятся дёшево, а их дети лежат компактно; на полном скане корпуса она медленнее — путь «форма → показатель → сумма» длиннее на один прыжок, и при полном охвате это не окупить ничем, кроме накопителей. Признаёмся: первая версия этого замера показала «12×» в пользу иерархии — это оказался артефакт запроса с LIMIT, который иерархический план исполнил потоково по трём формам, а плоский — целиком; после снятия LIMIT числа выше.

Диапазон по хранению, который стоит запомнить: от 7% (широкая запись, узкий разрез) до 40% (узкая запись, ёмкий разрез) — и знак меняется на минус, если нарушить правило частотности.

Результат 2: агрегаты по размерностям — кратное ускорение

«Сколько транзакций по каждому филиалу за неделю», «сколько по каждому типу за месяц» — запросы, которые читают только размерности:

Запросflathier2hier4
Счёт по филиалам, 7 дней2,6 с0,05 с (53×)0,19 с (14×)
Счёт по типам мерчанта, 30 дней10,5 с3,6 с (2,9×)

Здесь иерархия делает ровно то, что обещала: вместо миллиона строк-реквизитов запрос ходит по тысячам строк разреза. Обратите внимание на порядок цифр hier2 против hier4 в первой строке: чем точнее разрез совпадает с формой запроса, тем сильнее выигрыш.

Результат 3: агрегаты по мерам — честный проигрыш и как его выкупить

«Сумма оборота по филиалам» читает не только размерности, но и меру — amount, а она осталась на нижнем уровне:

Запросwideflathier4hier4 + накопители
Сумма по филиалам, 7 дней0,3 с2,7 с9,5 с0,8 с
Сумма по валютам, 90 дней1,3 с28,3 с40,1 с4,7 с

Иерархия здесь медленнее плоской раскладки в 1,4–3,5 раза. Причина — локальность: в плоской раскладке реквизиты транзакции лежат рядом с ней и читаются последовательно, а путь «разрез → транзакции → мера» скачет по таблице случайным доступом.

Лечится это третьей частью исходной идеи — выносом агрегатов наверх, только выносить надо не размерность, а накопитель меры: реквизит разреза «сумма детей», инкрементируемый при вставке. Тогда сумма по филиалам считается вообще без спуска на нижний уровень — 0,8 с против 2,7 с у плоского, 4,7 с против 28,3 с на квартале. Цена: +1 обновление на каждую вставку транзакции и горячая строка на популярной группе — это уже не «ничем не жертвуя», и мы это прямо фиксируем.

Результат 4: OLTP не страдает

Точечный поиск транзакции по PAN и выборка полной карточки — 3 мс во всех квартетных раскладках, включая иерархические (карточка собирается из двух уровней — свои реквизиты плюс подъём к разрезу). Вставка в иерархию требует найти-или-создать группу — это один индексный поиск по (t, val).

Честные границы

  • Широкая таблица всё ещё вне конкуренции по компактности и простым агрегатам: 0,46 ГБ против 5,5–6,1 ГБ и 0,3–1,3 с на запросах. Унификация покупает другое — схему, которая меняется без DDL и одинакова для всех приложений. Иерархия сокращает разрыв на аналитике, не отменяя его.
  • Замер — один корпус, одна машина, 1 млн транзакций без партиций; на 703 млн с партициями абсолютные числа будут другими (относительные соотношения — проверять там же).
  • Разрез затачивается под форму запросов: hier2 ускоряет «по филиалам» в 53 раза, но «по типам» для него — как у плоского. Уровней может быть больше двух — (дата, филиал) → (валюта, тип) — этот вариант мы ещё не мерили.
  • Генерация данных синтетическая (поля независимы); на реальных корреляциях («филиал работает в одной валюте») групп меньше, а выигрыш больше.

Выводы

Иерархический OLTP — это не «бесплатная оптимизация», а инструмент с тремя режимами:

  1. Размерности в разрез — кратное ускорение агрегатов по этим размерностям и экономия строк от 7% (широкая транзакция) до 40% (Ф110-профиль), пока соблюдается правило частотности.
  2. Слишком мелкий разрез — проигрыш по всем статьям; правило частотности проверяется до внедрения простым подсчётом кардинальностей.
  3. Накопители мер на разрезе — превращают квартеты в честную HTAP-схему: OLTP внизу, OLAP по верхнему уровню, ценой инкремента на вставке.

С данными в такой структуре работают ИИ-агенты, и им действительно «не сложно расковырять» её и накликать запрос — правила выбора разреза мы оформили в базу знаний как проверяемый чек-лист. Скрипты генерации и замера — в репозитории, числа перепроверяются одной командой.

← Все выпуски
Выпуск № 077