Pull to refresh

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 это не «прокинуть строку в БД», а склеить три разнородных слоя:

  1. Пивот — собрать Department (ссылка на listitems) и Age из _values в плоские колонки CTE.

  2. JOIN на справочникDepartment.Value это уже listitems._value, не listitem-id, поэтому добавляется LEFT JOIN list_items.

  3. Группировка + 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 живёт на плоских колонках CTEAVG(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 на уровне бд
примеры и архитектура
если это

redb examplt
redb examplt

разложить на классические таблицы + справочники+ таблицы связей+FK+индексы на FK+бизнес индексы, то получим, да серьезное приложение вместо одного SaveAsync(IEnumerable)

на одном реальном проекте(построенном на redb.route и redb.tsak) и классическом EF, есть внутри три проекта

  1. Абстракция EF слоя DAL

  2. Реализация слоя DAL на PG EF с DBContext конечно

  3. Проект внутри интеграции с системой САП три точки интеграции

это всё много тысяч строк кода + не один месяц работы = деньги бизнеса

все это еще надо поддерживать и отлаживать

по интеграции не приходят CRUD надо самому выравнивать с БД потому появились поля хешей с индексами конечно. и занимает сохранение на три ноды сотню объектов(весьма сложных) до >10 сек, проект большой делался более года.
кто этим занимался поймёт.

вся эта шляпа вместо одного SaveAsync(IEnumerable), да именно так.

теперь другой проект с кучей интеграций тоже на redb.route redb.tsak но вот бд полностью redb

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

и сделан он буквально за месяц = деньги

считаем человеко-часы не тока на разработку но и на поддержку, добавmте пару полей или таблицы в интеграции к проду... весело, однако у нас это занимает 10-15 минут.
далее немного картинок, данные собрались за ~месяц

redb.Objects
redb.Objects
redb.Values
redb.Values

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

redb prod
redb prod

сохранение пришедших по интеграции объектов(композитных и весьма),

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

redb prod
redb prod

и никаких EF и DBContext и миграций и никаких всё в строчку, и никаких DAL, но строго типизировано, потому и быстро индексы отрабатывают , здеся про это расказано

аудит пользователей реализован плоской таблицей, пихать его в redb.object антипаттерн

Джуны собрали проект за день с БД LDAP сессиями и прочее.

я помог засунуть в redb.tsak и вот у них хоть и простое решение, но оно сразу готово интегрироваться в систему предприятия, утром постановка задачи вечером презентация, и с БД полноценно, причем сразу с управлением с графанами прометеусами OTel джагерами и докерами, то есть уровень Enterprise , с хвостом для k8

redb.tsak
redb.tsak

к сожалению картинок более старых проектов (для 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, но там много фич в планах
я просто всё не успеваю физически, на подходе идентити сервер, но он пока на тестах на прод не несли

днем основная работа, никто не отменял

деревья с linq, самый смак тама в них, есть и полиморфные деревья

самое интересное я знаю во сколько обошлась компании экономя средств на проекте на большом, реально даже не один лям.

ну и если честно мне тоже не нравятся лозунги
- всё равно его не брошу потому что он хороший
- этого не может быть, потому что не может быть
- миграции это важный и сложный процесс

- риски потому что риски


здесь да, теряется из проекта что то такое большое сложное и важное, это БД с EF

если кто спросит а сколько у тебя таблиц в БД у тебя же сотни сущностей, то наверное да, ответить нечего..., да нисколько.

попробую тоже резюмировать
я не призываю отказываться от EF и даппера и нет проблем,
я просто показываю что это вполне рабочий вариант и как оно помогает достичь конечной цели бизнеса, быстрее и качественнее, а не строить БД ради БД

PS в моих словах нет критики или сарказма, наверное надо понимать что я вот это построил имея некоторые так сказать знания работы(и внутренностей) EF и прочих Hibernate

вся эта история прекрасно вяжется с
redb.route - альтернатива MassTransit  и прочих басов

redb.tsak - альтернатива karaf и прочих роуте контейнеров

и всё-же я ожидал не только критику, но странно почему нет вопросов, обычно если туда погрузится, тема не простая, и вопросов должно возникнуть масса...

Sign up to leave a comment.

Articles