Comments 10
Вся архитектура _objects/_values/_structures – классический EAV. Он убирает боль миграций, но создаёт другую: база непрозрачна для всего, что живёт вне приложения – BI-инструменты, ad-hoc запросы DBA, pg_stat_statements. В production это обнаруживается в первый инцидент, когда нужно быстро залезть в базу руками.
Granulex, спасибо за комментарий, но по существу — это не EAV. Похоже визуально, но архитектурно другое. Давайте на конкретике.
Classic EAV vs то, что под капотом у redb
Classic EAV — это (entity_id, attribute_name, value), где value — одна-единственная varchar/text колонка, куда сериализуется всё подряд. Тип «зашит» в строку attribute_name либо вообще нигде не записан. Отсюда боль: WHERE value > '100' сравнивает строки, индексов по типам нет, BI видит мусор.
В redb всё иначе. Это типизированный column store + полноценное RTTI на уровне БД + сеть FK. Разбираю по пунктам.
1. Типизированные колонки в _values — не “всё в строку”
В _values для каждого .NET-типа отдельная колонка с правильным SQL-типом:
_String text
_Long bigint -- int, long, enum
_Numeric numeric(38,18) -- decimal без потерь точности
_Double double precision
_DateTimeOffset timestamptz
_Boolean boolean
_Guid uuid
_Object bigint -- FK на _objects (ссылки на RedbObject<T>)
_ListItem bigint -- FK на _list_items (справочники)
_ByteArray bytea
Каждое свойство пишется в свою типизированную колонку. WHERE Long > 1000 идёт по обычному B-tree, WHERE DateTimeOffset >= '2026-01-01' — то же самое. Никакого CAST(value AS bigint) поверх text-колонки, как в classic EAV.
2. FK везде, не только индексы — целостность на уровне БД
Это, наверное, самое важное и редко встречающееся в EAV-системах. Реальный DDL из redbPostgre.sql:
CREATE TABLE _values (
...
CONSTRAINT FK__values__objects FOREIGN KEY (_id_object) REFERENCES _objects (_id) ON DELETE CASCADE,
CONSTRAINT FK__values__structures FOREIGN KEY (_id_structure) REFERENCES _structures(_id) ON DELETE CASCADE,
CONSTRAINT FK__values__array_parent FOREIGN KEY (_array_parent_id) REFERENCES _values (_id) ON DELETE CASCADE,
CONSTRAINT FK__values__list_items FOREIGN KEY (_ListItem) REFERENCES _list_items(_id),
CONSTRAINT FK__values__objects_ref FOREIGN KEY (_Object) REFERENCES _objects (_id)
);
CREATE TABLE _structures (
...
CONSTRAINT FK__structures__structures FOREIGN KEY (_id_parent) REFERENCES _structures(_id) ON DELETE CASCADE,
CONSTRAINT FK__structures__schemes FOREIGN KEY (_id_scheme) REFERENCES _schemes(_id),
CONSTRAINT FK__structures__types FOREIGN KEY (_id_type) REFERENCES _types(_id),
CONSTRAINT FK__structures__lists FOREIGN KEY (_id_list) REFERENCES _lists(_id),
CONSTRAINT FK__structures__collection_type FOREIGN KEY (_collection_type) REFERENCES _types(_id),
CONSTRAINT FK__structures__key_type FOREIGN KEY (_key_type) REFERENCES _types(_id)
);
CREATE TABLE _objects (
...
CONSTRAINT FK__objects__objects FOREIGN KEY (_id_parent) REFERENCES _objects(_id) ON DELETE CASCADE,
CONSTRAINT FK__objects__schemes FOREIGN KEY (_id_scheme) REFERENCES _schemes(_id) ON DELETE CASCADE,
CONSTRAINT FK__objects__users1 FOREIGN KEY (_id_owner) REFERENCES _users (_id),
...
);
Удалили объект — каскадом удалились все его свойства. Удалили scheme — каскадом всё, что на ней висит. Удалили scheme-родителя — children почистились (_id_parent → structures.id ON DELETE CASCADE). Поссылочный RedbObject<T> — _Object тоже FK. В classic EAV ничего такого нет, потому что там нет понятия «структура» как first-class сущности.
И всё это покрыто индексами:
CREATE INDEX "IX__values__objects" ON _values (_id_object);
CREATE INDEX "IX__values__structures" ON _values (_id_structure);
CREATE INDEX "IX__values__array_parent_id" ON _values (_array_parent_id);
CREATE INDEX "IX__values__array_parent_index" ON _values (_array_parent_id, _array_index);
CREATE INDEX "IX__values__array_key" ON _values (_id_structure, _array_index) WHERE _array_index IS NOT NULL;
CREATE INDEX "IX__objects__schemes" ON _objects (_id_scheme);
CREATE INDEX "IX__objects__objects" ON _objects (_id_parent);
CREATE INDEX "IX__objects__hash" ON _objects (_hash);
CREATE INDEX "IX__objects__value_long" ON _objects (_value_long) WHERE _value_long IS NOT NULL;
CREATE INDEX "IX__objects__value_datetime" ON _objects (_value_datetime) WHERE _value_datetime IS NOT NULL;
-- и т.д.
То есть план запроса с idscheme + idstructure лочит ровно нужные строки через covering-индекс, а не делает seq scan на гигантский _values.
3. Полноценное RTTI прямо в БД — C# не обязателен
Это вторая часть, которой нет в EAV вообще. У redb схема описана как first-class данные:
_types— таблица типов (Long, String, Numeric, DateTimeOffset, Guid, Boolean, Object, ListItem, ByteArray, Array, Dictionary, Class, JsonDocument, XDocument). Это отдельная справочная таблица, не C#-enum.schemes— схемы (имя, тип через FK наtypes, версия, родительская схема для наследования).structures— поля схем (имя, тип через FK наtypes, признак коллекции через FK наtypes, тип ключа словаря через FK наtypes, ссылка на child-scheme для вложенных POCO).
То есть БД сама знает, что EmployeeProps.OfficeLocations — это Dictionary<string, Address>, где Address — отдельная scheme с полями City: text и Street: text. И всё это видно одним SELECT:
SELECT
s._name AS field,
t._name AS type,
ct._name AS collection_type, -- Array / Dictionary / NULL
kt._name AS key_type, -- для Dictionary
cs._name AS child_scheme -- для вложенных POCO / RedbObject<T>
FROM _structures s
JOIN _schemes sc ON sc._id = s._id_scheme
JOIN _types t ON t._id = s._id_type
LEFT JOIN _types ct ON ct._id = s._collection_type
LEFT JOIN _types kt ON kt._id = s._key_type
LEFT JOIN _schemes cs ON cs._id = s._id_parent
WHERE sc._name = 'Employee';
DBA, BI-инженер, аудитор — открыл psql, получил полное описание схемы без единой строки .NET-кода. C# вообще не обязателен — фасеты и схемы можно строить из Python / Node / Go / Java / raw SQL. Schema-as-data, без хождения в приложение.
4. Теперь про DBA-инструменты — четыре конкретных ответа
4.1. get_object_json(id, depth) — встроенная SQL-функция в обоих диалектах:
SELECT get_object_json(42, 3); -- объект 42, глубина 3
SELECT get_object_json(o._id, 5) FROM _objects o WHERE _id_scheme = 123;
Полный граф объекта как JSON: вложенные структуры, массивы, словари, ссылки. «Дай мне этот заказ как есть» — одна строка SQL.
4.2. Полиглот-API. Free-движок строит SQL внутри БД, фильтры можно слать как JSON-фасеты из любого языка:
SELECT get_objects_json(
42::bigint,
'{"$and":[{"Age":{"$gt":30}},{"City":{"$eq":"London"}}]}'::jsonb,
100, 0
);
Python (psycopg), Node (pg), Go (pgx), Java (JDBC), raw psql — .NET не нужен.
4.3. pg_stat_statements работает. Pro-движок генерирует параметризованный SQL с $1..$N placeholders — стабильная форма, статистика агрегируется нормально. Free-движок строит SQL внутри plpgsql — в статистике видно вызовы pvt_build_query_sql() / get_objects_json() (менее гранулярно, но не ломается).
4.4. Свои таблицы рядом — без ограничений. ExecuteAsync с произвольным DDL, FK на objects(id) с cascade delete, материализованные view поверх get_object_json для аналитики. Это типичный паттерн для аналитического слоя.
Что не получите
Power BI Direct Query с авто-маппингом всех колонок «прозрачно как реляционная схема» — да, не получите, и не должны. Для аналитического слоя строятся свои витрины (как и в любой системе с EAV-подобным storage, и в большинстве микросервисных архитектур тоже). redb это не запрещает — просто это твой код, а не автогенерация.
Подытог
EAV — это (entity, attribute, value::text) без типов, без FK, без RTTI. У redb:
типизированные колонки под каждый .NET-тип,
FK через всю схему с CASCADE — целостность гарантирует сама БД,
типы как first-class данные в
types/schemes/_structures— схема видна из SQL без C#,встроенные функции для DBA-сценариев
get_object_json/get_objects_json.
Два года в проде у двух заказчиков (HoReCa-дистрибьютор + аналитическая платформа, ~672k объектов / ~8M свойств). Инциденты были, лазили руками — get_object_json ровно для этого и пишется. Не сказал бы, что больнее, чем дебажить реляционную базу с 30 таблицами через .Include().
GroupBy + HAVING теперь в Pro (был только во Free с 1.2.x).
Казалось бы — обычный SQL HAVING, что тут писать статью. Но в redb это не «прокинуть строку в БД», а склеить три разнородных слоя:
Пивот — собрать
Department(ссылка наlistitems) иAgeиз_valuesв плоские колонки CTE.JOIN на справочник —
Department.Valueэто ужеlistitems._value, неlistitem-id, поэтому добавляетсяLEFT JOINlist_items.Группировка + HAVING — поверх плоских колонок CTE, но с агрегатами по объектам, а не по строкам
_values.
LINQ:
var stats = await service.Query<EmployeeProps>()
.GroupBy(p => p.Department.Value) // 1. ключ — алиас списка
.Where(g => g.Count() > 10 && g.Average(p => p.Age) < 40) // 2. HAVING
.Select(g => new { Dept = g.Key, N = g.Count() }) // 3. проекция
.ToListAsync();
Что генерируется на PG.Pro:
WITH pvt_cte AS (
SELECT v._id_object,
(array_agg(v._listitem) FILTER (WHERE v._id_structure = $1))[1] AS "Department_id",
(array_agg(v._Long) FILTER (WHERE v._id_structure = $2))[1] AS "Age"
FROM _values v
JOIN (SELECT _id FROM _objects WHERE _id_scheme = $3) o ON v._id_object = o._id
WHERE v._id_structure = ANY($4)
GROUP BY v._id_object -- группировка по объекту в CTE
)
SELECT li._value AS "Dept", COUNT(*) AS "N"
FROM _objects o
JOIN pvt_cte pvt ON o._id = pvt._id_object
LEFT JOIN _list_items li ON li._id = pvt."Department_id" -- единственный JOIN к справочнику
GROUP BY li._value -- группировка по value, не по id
HAVING COUNT(*) > $5 AND AVG(pvt."Age") < $6; -- HAVING на flat-колонках CTE
-- $1 = 130 (structure id of Department)
-- $2 = 101 (structure id of Age)
-- $3 = 42 (scheme id Employee)
-- $4 = {130, 101}
-- $5 = 10
-- $6 = 40
Заметные детали:
Department в
valuesхранится какlistitem(FK наlistitems) — не строка. ПоэтомуDepartment.Valueтребует JOIN, а не текстового сравнения, и индекс поlistitems._idотрабатывает.GROUP BYидёт поli._value, не поpvt.Department_id— потому что одинаковое значение могут шарить несколько list-item’ов, и пользователь группирует по бизнес-смыслу, а не по id.HAVINGживёт на плоских колонках CTE —AVG(pvt."Age"), а неAVG(_Long). Это позволяет SQL-оптимизатору переставлять предикаты как обычно.Параметризация полная —
$1..$6, форма запроса стабильна → план кэшируется PostgreSQL.
То же самое теперь работает на MSSql.Pro (через MAX(CASE WHEN …)), на PG Free и MSSql Free (с инлайн-литералами через format %L / QUOTENAME соответственно). GroupByHavingTestsBase: 33/33 HAVING + 6/6 без HAVING на всех четырёх тирах, identical row counts везде.
Если LINQ вам нравится больше, чем сырой SQL с CTE — этот один пример хорошо показывает, во что компилятор разворачивает три строчки .GroupBy(...).Where(...).Select(...) и почему просто «прокинуть в Postgres» тут не получилось бы.
Почему сложные графы быстрее — не маркетинг, а механика
Три конкретных причины:
1. Чтение — всегда из двух таблиц, не из N
В EF Core граф из 28 сущностей = 28 таблиц = JOIN-каскад или N+1 запросов. Планировщик PostgreSQL/MSSQL должен соединить 28 источников, построить план, выбрать стратегию join для каждой пары.
В redb — всегда objects + values, независимо от глубины графа. Планировщик работает с двумя таблицами с предсказуемыми индексами: (id_scheme), (id_object), (id_structure). Один CTE, один проход.
2. Запись — bulk COPY, не каскад INSERT/UPDATE
EF Core при сохранении графа генерирует цепочку INSERT/UPDATE по каждой таблице в правильном порядке (из-за FK). 28 сущностей = минимум 28 round-trip'ов или сложный батч с соблюдением порядка зависимостей.
redb при SaveAsync собирает все изменённые _values-строки в один список и пишет их через COPY (PostgreSQL) или SqlBulkCopy (MSSQL) за один вызов. Один network round-trip на весь граф любой глубины.
3. Материализация — параллельно по веткам графа
Pro-режим: при LoadAsync граф загружается через Parallel.ForEach — каждая ветка объектного дерева материализуется в своём потоке. Ветка order.Items и ветка order.Shipping читаются одновременно, не последовательно.
В EF Core с .Include() это в принципе невозможно без ручного разбиения на несколько запросов — DbContext не thread-safe.
Аргумент “в проде N лет в XXX крупнейших мировых компаниях, бла-бла…” особо веса не добавляет, а скорее даже больше раздражает. За свой многолетний опыт “в проде N лет” и “в крупных компаниях” я видел (и, каюсь, делал) огромное количество самых разных костылей, поделок и нашлёпок на разные БД, от которых у многих кровища из глаз польётся при первом же взгляде. Но оно работает, десятки лет! При чём вполне успешно.
Касательно redb, работа проделана большая. И как бы в комментариях не убеждали в обратном, но всё же это EAV. Да не топорный классический, а с со своим переподвыподвертом. Спасибо, но нет :)
Насчёт EF, проблем с Include лично я не вижу, если их не использовать :) В общем, не нужны они кроме как для несложных одноразовых приложений. А вытягивать графы объектов в память приводит к большим издержкам. Улучшать работу с тем, чего на самом деле нужно избегать, не выглядит для меня плюсом, а скорее жирным минусом.
не много не согласен
похож на EAV но всё же не так
подход несколько другой RTTI на уровне бд
примеры и архитектура
если это

разложить на классические таблицы + справочники+ таблицы связей+FK+индексы на FK+бизнес индексы, то получим, да серьезное приложение вместо одного SaveAsync(IEnumerable)
на одном реальном проекте(построенном на redb.route и redb.tsak) и классическом EF, есть внутри три проекта
Абстракция EF слоя DAL
Реализация слоя DAL на PG EF с DBContext конечно
Проект внутри интеграции с системой САП три точки интеграции
это всё много тысяч строк кода + не один месяц работы = деньги бизнеса
все это еще надо поддерживать и отлаживать
по интеграции не приходят CRUD надо самому выравнивать с БД потому появились поля хешей с индексами конечно. и занимает сохранение на три ноды сотню объектов(весьма сложных) до >10 сек, проект большой делался более года.
кто этим занимался поймёт.
вся эта шляпа вместо одного SaveAsync(IEnumerable), да именно так.
теперь другой проект с кучей интеграций тоже на redb.route redb.tsak но вот бд полностью redb
проект весьма не простой по бизнесу, тока вот бизнес не успевал с идеями за нами.
и сделан он буквально за месяц = деньги
считаем человеко-часы не тока на разработку но и на поддержку, добавmте пару полей или таблицы в интеграции к проду... весело, однако у нас это занимает 10-15 минут.
далее немного картинок, данные собрались за ~месяц


к этим показанным выше значениям рест запрос на бэкенд, 6 агрегаций(весьма сложных) сортировки, фильтрация не по одному полю, время выполнения всего рест запроса 0.16 сек

сохранение пришедших по интеграции объектов(композитных и весьма),
1447 объектов заняло 1.3 сек да, я даже не замарачиваюсь что тама поменялось, оно само всё сравнит и сделает потоки балк на сохранения, просто вызываю SaveAsync(IEnumerable(1447))

и никаких EF и DBContext и миграций и никаких всё в строчку, и никаких DAL, но строго типизировано, потому и быстро индексы отрабатывают , здеся про это расказано
аудит пользователей реализован плоской таблицей, пихать его в redb.object антипаттерн
Джуны собрали проект за день с БД LDAP сессиями и прочее.
я помог засунуть в redb.tsak и вот у них хоть и простое решение, но оно сразу готово интегрироваться в систему предприятия, утром постановка задачи вечером презентация, и с БД полноценно, причем сразу с управлением с графанами прометеусами OTel джагерами и докерами, то есть уровень Enterprise , с хвостом для k8

к сожалению картинок более старых проектов (для WB\Озон\RealEsatet делалось) у меня нет, не коллекционирую, показываю вот прям что счас у меня есть (без всяких яких).
если кому интересно могу нарезать с прода много картинок, с замерами всякими.
но есть нюанс, с нейронкой если ей всё грамотно показать, проекты делаются в разы еще быстрее, потому что работа только с бизнес сущностями и бизнес процессами.
Я вас по-человечески уважаю, поэтому отвечать буду сам, без нейронки.
Теперь по существу.
До сих пор не понимаю, почему вы упорно сравниваете своё решение redb с EF. Вы сравниваете тёплое с мягким. Почему? И зачем?
EF это ORM. А redb это сильный vendor lock, создающий надструктуру над существующей структурой хранения данных, которая полностью замыкает работу с БД на себя. И вместо борьбы со сложностью, которую решает EF, redb создаёт совершенно новый уровень сложности, с сомнительным выхлопом.
Я могу EF заменить на linq2db, на Dapper, могу работать с БД естественным образом на любых других языках. С БД одинаково удобно можно работать на всех уровнях: DE, DS, DBA. Мне не нужно обеспечивать дополнительные трансформации из неведомой структуры, которую никто в мире не знает, не понимает и не умеет с ней работать.
Использование redb вносит в проект огромные, просто колоссальные и существенные риски в его судьбу.
Взамен же я получаю примерно ничего. Описанные вами сложности, с которыми призвано бороться redb, может и правда в вашей практике они есть и были, но ни в моей практике, ни в практике моих коллег я не помню, чтобы ощутимые затраты на разработку сидели в области работы с EF (или другим ORM).
Резюмирую. Нет, лично у меня нет никаких проблем с EF. У моих команд нет проблем с EF, на это не тратится какое-то существенное время. Оно тратится на бизнес-логику, алгоритмы, интеграции, оптимизации, надёжность, наблюдаемость и т.д.
спасибо, таки да и без нейронки общаемся, без заученных книжных оборотов.
почему я позиционирую как альтернативу EF
конечно внутренности обсолютно разные
но это призвано решать тот же класс бизнес задач только значительно меньше гемороя
и я не говорю что надо все делать, как раз подчеркиваю что плоские данные и тренды лучше хранить плоскими таблицами, не зачем пихать тренд температуры в redb однако сложные композитные объекты требуют ну очень много времени на написание кода и поддержки с кучей FK и багов и нужных индексов, и CRUD превращается для них ( а особенно если их сотни и тысячи идут) тыжелое бремя как в коде так и скорости выполнения...
прредставьте вам пришел объект внутри классы массивы элементы справочников во внутренних классах массивы классов внутри тоже есть Dictionary таки еще с ключом tuple ну в примере я показал на сайте redb.ru, и его надо не только сохранить но найти в нем изменения и апдетить удалять что еще? ах да, кончно таблицы связей, кудаж без них то, вопрос сколько будет делаться интеграция?
но вот вы наконец сделали всё и отладили с помощью нейронки быстрее да, и отнесли на прод
и к вам приходит бизнес и говорит что вашу большую сущность надо расширить массивом строк массиовом чисел и массивом внутренних классов.... это стоит очень дорого потому как регресс.. полный...
в случае с redb ничего не стоит
но если вам надо вывернуть плоско на бд таки это один запрос с одним join к таблице кеша RTTI ну или пару join соберите сами типы и структуры и схемы..
это очень просто, но чтоб не мучится в redb есть функции и их много и одна из низ get_object_json(id) вернет вам собранный граф за миллисекунды
тоесть пока делает проект команда слоя DAL, с redb давно делают бизнес функционал и презентуют в этот же день прототип..
подход разный с EF - но я сравниваю скорости какие запросы можно сделать на linq для redb, а тама не уступает но гдето возможно и превосходит, но есть элементы которые не реализованы по сравнению с EF
потому я исхожу из того что время деньги, бизнесу всё равно как вы храните,
про риски, риски так же оценивали и другие - суть: это не понятно и не занкомо и возможно сложно.
вот и всё.
по сути данную систему в сложных проектах используют как гибрид с даппером, линейные таблицы таки да ...
но в redb есть не только crud но и разделение по доменам соединений, кеш даже с квотами, экспорт импорт (бэкап) , встроенные деревья с linq
отсечение на уровне объектов whereRedb и я уже не лпомню всего, этот проект уже давно в работе, новые фичи, вот выпускаю для sqlite, но там много фич в планах
я просто всё не успеваю физически, на подходе идентити сервер, но он пока на тестах на прод не несли
днем основная работа, никто не отменял
ну и если честно мне тоже не нравятся лозунги
- всё равно его не брошу потому что он хороший
- этого не может быть, потому что не может быть
- миграции это важный и сложный процесс
- риски потому что риски
здесь да, теряется из проекта что то такое большое сложное и важное, это БД с EF
если кто спросит а сколько у тебя таблиц в БД у тебя же сотни сущностей, то наверное да, ответить нечего..., да нисколько.
попробую тоже резюмировать
я не призываю отказываться от EF и даппера и нет проблем,
я просто показываю что это вполне рабочий вариант и как оно помогает достичь конечной цели бизнеса, быстрее и качественнее, а не строить БД ради БД
PS в моих словах нет критики или сарказма, наверное надо понимать что я вот это построил имея некоторые так сказать знания работы(и внутренностей) EF и прочих Hibernate
вся эта история прекрасно вяжется с
redb.route - альтернатива MassTransit и прочих басов
redb.tsak - альтернатива karaf и прочих роуте контейнеров
и всё-же я ожидал не только критику, но странно почему нет вопросов, обычно если туда погрузится, тема не простая, и вопросов должно возникнуть масса...
redb — типизированное хранилище для .NET поверх Postgres/MSSQL: без миграций, без Include, с полным LINQ