惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyberwarzone
Cyberwarzone
Vercel News
Vercel News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
A
About on SuperTechFans
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿
腾讯CDC
S
SegmentFault 最新的问题
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Hacker News
The Hacker News
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
B
Blog
IT之家
IT之家
Spread Privacy
Spread Privacy
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cisco Blogs
Recent Announcements
Recent Announcements
H
Hacker News: Front Page
AI
AI
I
InfoQ
H
Heimdal Security Blog
T
Threatpost
Cisco Talos Blog
Cisco Talos Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
I
Intezer
W
WeLiveSecurity
SecWiki News
SecWiki News
MongoDB | Blog
MongoDB | Blog
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
云风的 BLOG
云风的 BLOG
T
Threat Research - Cisco Blogs
V2EX - 技术
V2EX - 技术
N
News and Events Feed by Topic
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
O
OpenAI News
阮一峰的网络日志
阮一峰的网络日志
T
Troy Hunt's Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
雷峰网
雷峰网
T
Tor Project blog
有赞技术团队
有赞技术团队
Schneier on Security
Schneier on Security
Last Week in AI
Last Week in AI

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
HTAP внутри OLTP: как мы строили векторизованный движок с самого начала
Сергей · 2026-05-08 · via Все публикации подряд на Хабре

Сложный

12 мин

6K

Все предыдущие статьи этого цикла крутились вокруг одной темы: как сделать OLTP предсказуемым. Мы разбирали p99 latency и почему оно уезжает при смешанной нагрузке. Писали про API-контракты между слоями ядра, про WAL-before-data как фундамент корректного recovery, про Buffer Pool с Clock-sweep и BufferRing, который изолирует полные сканы от горячего рабочего набора. Всё это фундаментальная инженерная работа для предсказуемого OLTP-ядра.

Но есть одна вещь, которую мы намеренно не трогали в предыдущих статьях.

С самого первого дня архитектура нашей базы закладывалась как HTAP-ready (Hybrid Transactional/Analytical Processing). Не «добавим аналитику потом», а «в ядре с нуля есть место для векторизованного выполнения». Это решение влияло на выбор типов данных, формат батчей, структуру планировщика. Эта статья про это направление.

Что реализовано на момент публикации: PhysicalType и колоночные батчи; SelectionVector; RowToColumnBridge и BatchToRowBridge; векторизованные VectorSeqScan и VectorIndexScan; SIMD-ускоренный lower-bound на листовых страницах B-Tree (под отдельным feature-флагом сборки на nightly Rust); автовекторизуемые компилятором фильтрационные кернелы для i64/f64 колонок на стабильной toolchain; векторизованный Hash Join и хэш-агрегации. Что ещё не готово: планировщик пока не умеет автоматически выбирать между VectorSeqScan и VectorIndexScan на основе актуальной статистики; колоночное хранилище на диске (данные сейчас row-store, конвертация происходит через RowToColumnBridge на лету); колоночные хэш-кернелы по типам уже есть, но в горячий путь Hash Join подключаются отдельным шагом.


Проблема: почему построчное исполнение плохо считает

Исторически OLTP-базы строились вокруг модели Volcano-итератора, или tuple-at-a-time execution. Каждый оператор плана выполнения передаёт следующему данные по одной строке таблицы. Для коротких транзакционных запросов вроде SELECT * FROM users WHERE id = 1 это работает отлично: нашли одну строку, вернули, закончили.

Проблема проявляется, когда нужно посчитать агрегацию по миллиону строк. Построчная модель начинает ощутимо проигрывать по нескольким причинам:

  • Косвенный вызов (dispatch) на каждую строку. Каждое next() в классическом Volcano — это обращение через vtable, и процессор плохо предсказывает такие ветвления (branch misprediction) при высоком темпе чередования операторов.

  • Плохая утилизация кэшей L1/L2. Строки в row-store лежат вперемежку с разными полями, а нам для агрегации часто нужна только одна колонка.

  • Нет возможности применить SIMD. Процессор умеет обрабатывать сразу несколько значений одной SIMD-операцией, но tuple-at-a-time pipeline не даёт ему такой возможности: данные поступают поштучно.

Для транзакционных запросов всё это незаметно: запрос затрагивает 1–10 строк. Но для аналитических сценариев (агрегации, соединения, сканирование диапазонов) векторизованный подход снижает накладные расходы по сравнению с построчным исполнением на тех же данных.


От Value к PhysicalType: как выглядят данные внутри векторизованного батча

В классическом построчном исполнителе каждое значение обычно представляется как enum, примерно вот так:

enum Value {
    Int(i64),
    String(String),
    Null,
}

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

Мы перешли на PhysicalType. Векторизованный батч хранит данные не по строкам, а по колонкам. Колонка Int64 — это непрерывный массив Vec<i64>; для горячих SIMD-путей выравнивание обеспечивается отдельно. Это позволяет загружать несколько значений в SIMD-регистры и обрабатывать их одной операцией, что повышает пропускную способность на подходящих архитектурах.

Батч в памяти имеет фиксированный верхний предел строк (по умолчанию 1024) и состоит из трёх частей:

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

  2. Null bitmap. Vec<u64>, где 1 бит отвечает за 1 строку (1 значит «значение есть», 0 значит NULL). Упаковка в слова по 64 бита позволяет работать с целыми группами строк через bitwise-операции вместо побитового цикла.

  3. SelectionVector.

Почему 1024 строки? Это компромисс между накладными расходами на вызов оператора и локальностью данных. Для колонки Int64 такой вектор занимает 8 КБ, то есть несколько горячих колонок и служебные структуры всё ещё помещаются в L1/L2 на типичных x86-64 CPU. Размер можно менять через настройку vector_batch_size, но верхняя граница 1024 оставлена как консервативное значение по умолчанию для кэшей и предсказуемого потребления памяти.

На последнем пункте стоит задержаться, потому что он нетривиален.


SelectionVector: как фильтры работают без копирования данных

Допустим, у нас батч из 1024 строк, и мы применяем фильтр WHERE age > 18. После фильтрации подходит, скажем, 300 строк. Как передать результат дальше?

Наивный вариант: удалить неподходящие строки из колонок, сдвинуть элементы. Это O(N) копирований на каждый фильтр, и при цепочке из пяти операторов мы платим за копирование пять раз.

Другой вариант: битовая маска (bitmask). Один бит на строку. Но при последующих операциях (хэшировании, проекции) каждый оператор вынужден распаковывать маску перед работой с данными.

Мы выбрали список индексов. SelectionVector — это Vec<u16>, который содержит индексы строк, прошедших фильтрацию: [0, 3, 7, 8, ...]. При последующих операциях следующий оператор итерируется по плотному массиву индексов, напрямую обращаясь к нужным элементам в колонках, без промежуточного распаковывания.

Этот выбор особенно хорош при умеренной и низкой селективности, когда после фильтра остаётся небольшая доля строк. При высокой плотности прошедших строк, например 900 из 1024, список индексов становится длинным, а доступ к колонкам через него хуже плотного последовательного прохода. В текущей реализации всегда используется SelectionVector; адаптивный выбор между SelectionVector и bitmask по наблюдаемой селективности — задача следующей итерации.


RowToColumnBridge: мост между row-store и векторизованным пайплайном

Наши данные на диске хранятся в формате строк таблицы (row-store). Это стандартное решение для OLTP: точечные чтения и запись по первичному ключу там оптимальны. Но как их передать векторизованному исполнителю?

Для этого есть оператор RowToColumnBridge. Он читает строки с диска (уже после проверки MVCC-видимости, так что векторизованному исполнителю не нужно ничего знать о транзакциях) и транспонирует их в колоночные батчи нужного формата. Проверку видимости делает scan-слой, который читает heap-страницы под snapshot: невидимые версии он отбрасывает до передачи строки в мост.

Да, здесь есть оверхед на конвертацию. Но даже с учётом этого транспонирования последующая векторизованная фильтрация и агрегация могут окупать стоимость моста. А ещё это архитектурный задел: когда данные будут лежать в колоночном формате на диске, этот мост заменится на оператор прямого колоночного чтения с минимизацией копирования; конкретная степень зависит от формата хранения, кодеков и структуры страниц. Весь остальной пайплайн (фильтры, соединения, сортировки) при этом останется неизменным.

RowToColumnBridge работает в потоковом режиме. Строки читаются батчами, каждый батч сразу транспонируется и передаётся вниз по пайплайну. Это позволяет не материализовывать весь результат в памяти и корректно завершать выполнение досрочно при наличии LIMIT.


SIMD в B-Tree: где векторизация помогает, а где нет

Полные сканы таблицы с векторизацией работают хорошо. Но OLTP-нагрузка — это прежде всего точечные запросы и короткие диапазоны по индексам. Можно ли векторизовать обход B-Tree?

Частично. Всё зависит от того, о какой части B-Tree идёт речь.

Спуск по дереву: здесь SIMD не помогает

Когда мы ищем ключ в B-Tree, первая фаза — спуск от корня до листовой страницы. Читаем корневую страницу, находим нужный указатель, спускаемся ниже, и так до листа. Это классический pointer-chasing: процессор упирается в задержку доступа к памяти, ожидая следующий узел дерева. SIMD ускоряет вычисления над плотным массивом данных, а здесь главный расход — ожидание следующей страницы.

Листовые страницы: где SIMD уже работает по данным

Листовая страница содержит плотный массив ключей. Вместо классического partition_point по одному элементу мы загружаем ключи в SIMD-регистры и одной операцией находим первую позицию, удовлетворяющую границе диапазона (lower-bound). Это ускоряет именно начало диапазонного скана — WHERE created_at BETWEEN x AND y, WHERE id >= ? — а дальше плотный последовательный проход по листу делает остальное. На нашем AVX2-стенде в синтетических тестах с диапазонами шириной от 16 ключей на страницу мы наблюдали ускорение 2–3× по сравнению со скалярным partition_point. Это не универсальная константа: конкретный коэффициент зависит от ISA, ширины регистра, типа данных и ширины диапазона.

Скалярный путь на листе выглядит как цикл по одному ключу:

for key in leaf.keys() {
    if key >= lower && key <= upper {
        tids.push(key.tid());
    }
}

Векторизованный путь делает то же сравнение группами: загружает несколько ключей в SIMD-регистр, получает маску совпадений и определяет позицию первой подходящей строки за одну операцию. Мы не пытаемся «векторизовать B-Tree целиком»; ускоряется именно плотный leaf-scan, где данные уже лежат рядом, и в первую очередь lower-bound поиск, с которого начинается любой диапазонный скан.

В текущей реализации std::simd-путь на листе включается отдельным feature-флагом сборки (он опирается на nightly-Rust API), а сборка по умолчанию использует скалярный fallback на стабильной toolchain. В горячих местах самого батчевого исполнителя (фильтры по i64 и f64 колонкам) мы намеренно полагаемся на autovectorization компилятора: код написан так, чтобы LLVM мог сам сгенерировать AVX2/NEON. Это даёт переносимый stable-build «по умолчанию» и явный nightly-путь там, где он действительно нужен.


VectorIndexScan: как индексный поиск вписывается в векторизованный пайплайн

Интеграция индексов в векторизованный движок потребовала нового оператора. Он называется VectorIndexScan, и его пайплайн работает в четыре шага:

Шаг 1. Потоковый обход листовых страниц. Итератор пробегает по листам B-Tree с применением SIMD-сравнений и собирает батч TID-ов (Tuple Identifier). Итератор ленивый: он не материализует весь результат в память, что позволяет корректно завершать выполнение досрочно при LIMIT.

Шаг 2. Heap fetch. По собранным TID-ам читаем сами строки из табличных страниц. Это случайный ввод-вывод, и его стоимость зависит от того, насколько TID-ы кластеризованы на диске. При низкой кластеризации (данные вставлялись не по порядку) этот шаг может стать узким местом.

Шаг 3. Проверка видимости и транспонирование. Строки проходят snapshot-based MVCC-проверку (здесь тоже есть накладные расходы, особенно при длинных цепочках версий) и транспонируются в колоночный батч через RowToColumnBridge.

Шаг 4. Residual filter. Векторизованный фильтр применяется к условиям, которые не вошли в индекс. Например, при WHERE indexed_col = 5 AND non_indexed_col > 10 индекс покрывает первое условие, а второе применяется уже на батче через SelectionVector.

Более честная оценка стоимости выглядит не как просто O(log N + K), а как O(log N + K × heap_fetch_cost + K × visibility_cost). Если TID-ы хорошо кластеризованы, heap_fetch_cost близок к последовательному чтению страниц; если плохо — это серия случайных обращений. Поэтому итоговая цена часто определяется не самим поиском в индексе, а кластеризацией TID-ов, локальностью heap fetch, длиной version chains и тем, покрывает ли индекс нужные колонки. При K в несколько тысяч строк heap fetch и MVCC-проверка могут составлять большую часть времени выполнения, даже если leaf-scan по индексу отработал быстро.


Когда SeqScan, а когда IndexScan: эвристика планировщика

У планировщика теперь есть осмысленный выбор между двумя операторами:

  • VectorSeqScan читает всю таблицу последовательно. Ввод-вывод при этом последовательный, что хорошо для дисковых носителей. Сложность O(N).

  • VectorIndexScan спускается по B-Tree и делает heap fetch по TID-ам. Ввод-вывод при этом может быть случайным. Стоимость ближе к O(log N + K × heap_fetch_cost + K × visibility_cost), где K количество подходящих строк.

В текущей реализации используется эмпирическая эвристика: на нашем стенде индексный путь начинает выигрывать на низкой селективности, когда запрос затрагивает только малую долю таблицы. При более высокой селективности часто дешевле последовательно прочитать таблицу с векторизованной фильтрацией, потому что последовательный ввод-вывод обычно эффективнее случайного. Точный порог сильно зависит от степени кластеризации данных, характеристик носителя, ширины строки и покрытия индекса, поэтому это не «универсальное правило 5%», а рабочая граница для конкретной конфигурации.

Дальнейшее развитие планировщика предполагает актуальную статистику и выбор на основе оценки стоимости, но для текущего состояния системы такая эвристика работает разумно.


Векторизованный Hash Join и агрегации

Помимо сканирования, на векторизованные операторы переведены соединения и агрегации. Это важно: даже если отдельные сканы быстрые, без векторизованного соединения выигрыш теряется при первом JOIN.

Hash Join разделён на две фазы. На build-фазе меньшая сторона соединения материализуется в хэш-таблицу; для HTAP-сценариев это важное ограничение, потому что build-сторона должна помещаться в доступный лимит памяти или оператор должен перейти в более сложный режим со сбросом на диск. На probe-фазе большая сторона приходит батчами: для каждой строки батча вычисляется ключ соединения, и затем выполняется обычный probe по хэш-таблице. Главный выигрыш здесь не в SIMD-хэшировании в горячем цикле, а в устранении косвенных вызовов между операторами: фильтрация и проекция перед соединением уже отработали колоночно по всему батчу. Реальная пропускная способность probe-фазы дополнительно зависит от структуры хэш-таблицы и локальности обращений к памяти. Колоночные хэш-кернелы для i64/bool/utf8 уже есть в коде, но подключение их в горячий путь Hash Join запланировано отдельным шагом.

Агрегации (GROUP BY, SUM, COUNT, AVG) работают аналогично: операции применяются к колонке целиком, а не к каждому элементу по отдельности. Null bitmap при этом обрабатывается через word-level операции над Vec<u64>, что быстрее побитового цикла, хотя проход по массиву слов всё равно необходим.


Что показывают замеры

Мы прогнали сравнительный бенчмарк на внутренней эталонной базе: локальный NVMe, Linux 6.12, Intel Core i5-12400, 32 ГБ RAM, buffer pool 2 ГБ, vector batch size 1024. Таблица seed_order_items содержит 2 250 000 строк и хранится в row-store. Сценарий ниже — warm-cache: перед серией запросов буферный пул прогрет, drop_caches не использовался. Это не cold-cache измерение «с чистого диска», а сравнение двух путей исполнения на одной и той же базе. Режимы включались перезапуском сервера с execution.mode = "force_row" и execution.mode = "force_vector".

Важно: все три запроса в таблице ниже — последовательные сканы. Это не бенчмарк VectorIndexScan; индексный путь зависит от селективности, кластеризации TID-ов и покрытия индекса, поэтому для него нужен отдельный набор измерений.

Для каждого запроса было 5 прогонов в каждом режиме. В таблице указаны среднее, стандартное отклонение по пяти прогонам и диапазон min–max:

Запрос

force_row

force_vector

Ускорение по avg

SUM(qty) + SUM(unit_price) + COUNT(*) по всей таблице

avg 1051 мс; σ 58; min–max 988–1129

avg 865 мс; σ 12; min–max 854–887

1.22×

COUNT(*) WHERE qty > 2 (фильтр без индекса)

avg 1068 мс; σ 43; min–max 990–1116

avg 766 мс; σ 17; min–max 736–788

1.39×

GROUP BY product_id + SUM(qty)

avg 2699 мс; σ 181; min–max 2494–3036

avg 1012 мс; σ 47; min–max 954–1088

2.67×

GROUP BY — это то место, где разрыв наиболее заметен: батчевая обработка ключей хэш-агрегации даёт 2.67× на той же row-store базе. Простые полнотабличные агрегаты дают 1.22–1.39×, что честнее отражает ситуацию с row-store на диске: часть выигрыша съедается конвертацией через RowToColumnBridge, чтением row-store страниц и кэш-поведением.

Несколько оговорок, которые важно держать в голове:

  • Данные хранятся в row-store. Когда появится колоночный формат на диске, RowToColumnBridge уйдёт и картина изменится в пользу векторизованного пути.

  • Порог переключения VectorSeqScan/VectorIndexScan подобран эмпирически на этом стенде. На другом профиле носителя, при другом размере строк или другой кластеризации TID-ов граница сдвигается.

  • Ускорение SIMD на листовых страницах B-Tree зависит от ISA и ширины диапазона. Отдельные замеры по этому пути будут в следующих материалах.


Как это держится вместе

Принципиальный момент в архитектуре: мы не добавляли векторизованное выполнение поверх существующего OLTP-ядра. PhysicalType вместо Value появился не потому что кто-то оптимизировал горячий путь, а потому что такой выбор был сделан на этапе дизайна типов. RowToColumnBridge спроектирован так, чтобы MVCC-слой не знал про колоночный формат вообще: он возвращает строки, а мост транспонирует их дальше. Это позволило держать OLTP-путь (точечные транзакции, pin/unpin, no-steal, WAL) и аналитический путь (батчи, SIMD, SelectionVector) в одном движке без взаимных зависимостей — слои разделены контрактами.

Что пока не сделано: планировщик не использует актуальную статистику для выбора оператора; данные на диске хранятся в row-store (переход на колоночный формат — отдельная задача с собственным форматом страниц и миграцией); оконные функции в векторизованном пайплайне не реализованы. Все эти пункты зафиксированы в дизайн-документах как следующие шаги.


Что проверять дальше

Главный открытый вопрос теперь не в том, можно ли встроить векторизованный пайплайн в OLTP-ядро. Можно. Интереснее посмотреть, как он ведёт себя на реальной смешанной нагрузке: с живыми индексами, неидеальной кластеризацией данных, длинными version chains, разными размерами buffer pool и запросами, которые меняются со временем.

Это направление — отдельный большой блок работы: как собрать воспроизводимый HTAP-бенчмарк, какие метрики смотреть кроме среднего времени запроса, почему один и тот же VectorIndexScan может быть быстрым на одном профиле данных и проигрывать последовательному скану на другом. К этим вопросам мы ещё вернёмся в следующих материалах цикла, по мере того как набираются собственные измерения и наблюдения с реальных стендов.


Закрытое бета-тестирование

Мы рассказываем технические детали и открываем закрытое бета-тестирование, чтобы проверить заявленные в статье решения на реальной нагрузке и не распыляться на «всё для всех». Никаких обязательств по закупкам, никакого enterprise-sales — это техническая проверка с обеих сторон.

Поддерживаемый стек на момент публикации:

  • PostgreSQL wire-протокол: simple query и extended query (Parse/Bind/Execute).

  • Драйверы: psql, psycopg3 (3.3.4), tokio-postgres (0.7.17, simple + extended), sqlx (0.8.6).

  • ORM/прикладные системы: Django 5.x на psycopg3, Odoo 19 (community).

  • Сборка: Linux x86_64, glibc ≥ 2.28 (пакеты .rpm и .deb, tarball).

Подходящий профиль команды:

  • сейчас в production стоит PostgreSQL и есть метрики по нему;

  • стек целиком ложится на список выше, без зависимости от хранимых процедур, триггеров и редких расширений;

  • есть инженер, который будет вести тестирование с вашей стороны и сможет договариваться об измерениях.

Сроки и формат:

  • Заявки принимаются до 25 мая 2026.

  • Дальше обсуждаем индивидуально с каждой командой: сроки, сценарий, объём данных, критерии успеха, что именно фиксируем в бенчмарк-сессии под NDA.

Если интересно поучаствовать — напишите на team@angarabase.com: коротко о вашей системе, стеке и сценарии, который хотите проверить.