Дополню про концентрацию вендора — она тут слабее, чем кажется. Ядро, движок, коннекторы и рантайм открыты: заброшенность лечится форком, supply chain — аудитом (а в век LLM прошерстить всё дерево это вечер, а не неделя). Pro-слой закрыт бинарём, но, во-первых, раздаётся бесплатно и без лицензионного сервера на всей линии 3.x — «рубильника» у вендора нет; во-вторых, компаниям, которым нужен исходник, я отдаю его по запросу. Так что и тут это не lock-in, а обычный риск «поставщик может остановиться» — с открытым ядром и доступным по запросу Pro это не тупик, а форк.
Согласен с самим принципом: изоляция отказов реальна, и если бы обновление шины насильно тянуло за собой перевыпуск хранилища — это был бы антипаттерн. Но здесь смешаны две ортогональные вещи: изоляция в рантайме и связность версий на сборке.
В рантайме изоляция никуда не делась. Шина, БД и рантайм — по-прежнему отдельные процессы (и отдельные NuGet-пакеты). Регрессия в шине не роняет хранилище: откатываете один пакет/образ шины, база живёт дальше. Ровно ваш сценарий «слегла шина — база живёт» работает как есть, откат — один слой.
«Единый бамп» — это рекомендация, а не жёсткая связность. Пакеты версионируются независимо и совместимы по API внутри 3.x: redb.Route 3.3.0 спокойно едет с коннектором 3.2.x — просто без свежих фиксов конкурентности. То есть «чтобы починить шину, придётся перевыпускать хранилище» фактически не так — бампаете только тот пакет, который трогали. Единая линия версий нужна тем, кому удобно ехать всем стеком разом, а не обязаловка.
А главный плюс синхронного выпуска — он гоняется тестами комплексно. Вся линия 3.3.0 проходит сквозную матрицу: БД, шина, коннекторы, рантайм проверяются в связке, на одном прогоне. В зоопарке такого прогона нет в принципе — каждый вендор тестирует себя у себя, а стык между ними (та самая интеграция версий) не тестирует никто, кроме вас, в проде. Синхронный релиз ровно эту дыру и закрывает: совместимость слоёв — гарантия линии, а не ваша ночная проверка.
И про «клей, который чинишь по ночам». В зоопарке по ночам обычно чинится не вендорский компонент, а именно клей между ними — адаптеры, контракты сериализации, мосты транзакций. Это ваш код: без вендора, без тестов, без SemVer. Здесь этот клей — тестированные версионированные примитивы одной линии с общей CI-матрицей. Регрессию ловит один прогон по всему стеку, а не «а совместим ли драйвер vX с шиной vY».
И ещё про «болеют по отдельности». Так они и лечатся по отдельности — разным стеком, разными людьми. Kafka чинит один спец, Postgres — DBA, Redis — третий, шину — четвёртый. Изоляция отказов оплачивается фрагментацией экспертизы: держать надо не один навык, а зоопарк компетенций, и в 3 ночи нужный человек не всегда на смене. Единая экосистема — это один стек знаний на всё, и правит его один человек, а не эстафета из четырёх дежурных. ну и вся эта история открытый исходный код.
Где вы правы по-настоящему: реальный компромисс не в изоляции отказов (она сохранена), а в концентрации вендора. Один поставщик на весь стек — это единый источник и багов, и рисков (заброшенность, вектор supply chain). Это честная цена за отсутствие интеграционного клея и за один стек компетенций, и держать её в голове стоит. Но это уже другой аргумент, чем «синхронный бамп ломает откат».
но опят же открытый исходный код в век нейронок прошерстить не составляет труда.
Тут небольшая путаница — redb.dll это не замена sqlite3.dll и не «другой SQLite», а загружаемое расширение SQLite (.load / sqlite3_load_extension). Оно не заменяет движок, а грузится поверх обычного SQLite: при загрузке через sqlite3ext.h резолвит API хоста и добавляет в базу функции redb (get_object_json, pvt_*-компилятор запросов, soft-delete, view прав).
Поэтому по пунктам:
— Быстрее? Некорректное сравнение: redb.dll использует ваш sqlite3 как есть и в его скорости ничего не меняет. Ценность не в скорости SQLite, а в том, что он превращает SQLite в типизированное объектное хранилище (серверный материализатор + компилятор запросов внутри БД — ровно то, что у Free-тира на Postgres/MSSql живёт как PL/pgSQL).
— Совместимо/взаимозаменяемо? Совместимо с SQLite (грузится в любой sqlite3 ≥ 3.44 — Microsoft.Data.Sqlite, python sqlite3, sqlite3 CLI), но не взаимозаменяемо: сам sqlite3.dll по-прежнему нужен, redb.dll к нему добавляется, а не вместо.
Если совсем коротко: sqlite3.dll — это движок, redb.dll — плагин к нему.
ну и если честно мне тоже не нравятся лозунги - всё равно его не брошу потому что он хороший - этого не может быть, потому что не может быть - миграции это важный и сложный процесс
- риски потому что риски
здесь да, теряется из проекта что то такое большое сложное и важное, это БД с EF
если кто спросит а сколько у тебя таблиц в БД у тебя же сотни сущностей, то наверное да, ответить нечего..., да нисколько.
попробую тоже резюмировать я не призываю отказываться от EF и даппера и нет проблем, я просто показываю что это вполне рабочий вариант и как оно помогает достичь конечной цели бизнеса, быстрее и качественнее, а не строить БД ради БД
PS в моих словах нет критики или сарказма, наверное надо понимать что я вот это построил имея некоторые так сказать знания работы(и внутренностей) EF и прочих Hibernate
вся эта история прекрасно вяжется с redb.route - альтернатива MassTransit и прочих басов
redb.tsak - альтернатива karaf и прочих роуте контейнеров
и всё-же я ожидал не только критику, но странно почему нет вопросов, обычно если туда погрузится, тема не простая, и вопросов должно возникнуть масса...
спасибо, таки да и без нейронки общаемся, без заученных книжных оборотов.
почему я позиционирую как альтернативу 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, но там много фич в планах я просто всё не успеваю физически, на подходе идентити сервер, но он пока на тестах на прод не несли
не много не согласен похож на EAV но всё же не так подход несколько другой RTTI на уровне бд примеры и архитектура если это
redb examplt
разложить на классические таблицы + справочники+ таблицы связей+FK+индексы на FK+бизнес индексы, то получим, да серьезное приложение вместо одного SaveAsync(IEnumerable)
на одном реальном проекте(построенном на redb.route и redb.tsak) и классическом EF, есть внутри три проекта
Абстракция EF слоя DAL
Реализация слоя DAL на PG EF с DBContext конечно
Проект внутри интеграции с системой САП три точки интеграции
это всё много тысяч строк кода + не один месяц работы = деньги бизнеса
все это еще надо поддерживать и отлаживать
по интеграции не приходят CRUD надо самому выравнивать с БД потому появились поля хешей с индексами конечно. и занимает сохранение на три ноды сотню объектов(весьма сложных) до >10 сек, проект большой делался более года. кто этим занимался поймёт.
вся эта шляпа вместо одного SaveAsync(IEnumerable), да именно так.
теперь другой проект с кучей интеграций тоже на redb.routeredb.tsak но вот бд полностью redb
проект весьма не простой по бизнесу, тока вот бизнес не успевал с идеями за нами.
и сделан он буквально за месяц = деньги
считаем человеко-часы не тока на разработку но и на поддержку, добавmте пару полей или таблицы в интеграции к проду... весело, однако у нас это занимает 10-15 минут. далее немного картинок, данные собрались за ~месяц
redb.Objects
redb.Values
к этим показанным выше значениям рест запрос на бэкенд, 6 агрегаций(весьма сложных) сортировки, фильтрация не по одному полю, время выполнения всего рест запроса 0.16 сек
redb prod
сохранение пришедших по интеграции объектов(композитных и весьма),
1447 объектов заняло 1.3 сек да, я даже не замарачиваюсь что тама поменялось, оно само всё сравнит и сделает потоки балк на сохранения, просто вызываю SaveAsync(IEnumerable(1447))
redb prod
и никаких EF и DBContext и миграций и никаких всё в строчку, и никаких DAL, но строго типизировано, потому и быстро индексы отрабатывают , здеся про это расказано
аудит пользователей реализован плоской таблицей, пихать его в redb.object антипаттерн
Джуны собрали проект за день с БД LDAP сессиями и прочее.
я помог засунуть в redb.tsak и вот у них хоть и простое решение, но оно сразу готово интегрироваться в систему предприятия, утром постановка задачи вечером презентация, и с БД полноценно, причем сразу с управлением с графанами прометеусами OTel джагерами и докерами, то есть уровень Enterprise , с хвостом для k8
redb.tsak
к сожалению картинок более старых проектов (для WB\Озон\RealEsatet делалось) у меня нет, не коллекционирую, показываю вот прям что счас у меня есть (без всяких яких).
если кому интересно могу нарезать с прода много картинок, с замерами всякими.
но есть нюанс, с нейронкой если ей всё грамотно показать, проекты делаются в разы еще быстрее, потому что работа только с бизнес сущностями и бизнес процессами.
Granulex Спасибо за развёрнутый ответ — взял в работу. В 3.1.1 (CHANGELOG уже обновлён) добавил per-message аудит-поля на MessageProps, проставляются движком на каждый persist:
PromptTemplateName + PromptTemplateVersion — на всех строках прогона, если caller передал managed template.
ToolSetHash — SHA-256 канонического набора {name, description, InputSchema} (отсортирован по имени), на assistant-строках. Меняется набор/схема инструмента — меняется хеш.
ProviderSystemFingerprint — system_fingerprint из ответа провайдера (OpenAI/xAI/Together эхо-возвращают; Anthropic-compat / Gemini-compat / Ollama чаще null).
Про закрытые модели честно: фингерпринта от Anthropic нет → bit-exact replay невозможен в принципе, фиксируем (model_id, params, tool_set_hash, prompt_template) и помечаем как best-effort. Для жёсткого compliance — self-hosted (Ollama / vLLM / llama.cpp), там воспроизводимость честная.
Миграции не было — REDB схему props подхватывает автоматически, просто добавил поля в класс.
Детали и code-paths: CHANGELOG.md → раздел 3.1.1, per-message audit fields on MessageProps
Что закрывается из коробки. Каждое сообщение в MessageProps несёт provider_id, model_id, stop_reason, токены и полный контент (text + tool_use + tool_result, без потерь). Промпты живут в отдельном сторе PromptTemplateProps с (name, version, body) — то есть «промпт v7 от 14 апреля» это конкретный объект в Postgres, который нельзя перезаписать незаметно.
Что НЕ закрыто, но важно для аудитора. На уровне per-message сейчас не пишется temperature/maxTokens и нет явной FK на (prompt_name, prompt_version). Это известная дыра — то самое «нащупали разрыв».
План — добавить в MessageProps поля Temperature, PromptTemplateName, PromptTemplateVersion, ToolSetHash (чтобы зафиксировать, какой набор tool'ов был активен на момент вызова). И вот тут как раз тот случай, где REDB снимает обычную боль: добавляешь поля в C#-класс props — и всё, никаких миграций, alter table, версий схемы и прочей светотени. Стор сам подхватывает новые атрибуты. То есть это не «архитектурное решение на спринт», а реально пара коммитов. Возьму в работу.
Что закрыть нельзя в принципе — drift на стороне провайдера. Anthropic может молча пере-зарелизить claude-sonnet-4-5, и вы про это узнаете только по поведению. Единственный честный ответ для compliance-доменов — либо self-hosted (vLLM / llama.cpp / Ollama, у нас есть алиасы), либо явный pinning через провайдеров, у которых это есть в API (xAI, OpenAI через system_fingerprint, Cohere). У Anthropic такого пока нет, и это плохо для финансов/медицины. Я закладываю это как явную лимитацию в архитектурный контракт, а не пытаюсь магией скрыть.
у нас то же, через ?conversation=header: id из хедера, история живёт в Postgres (ConversationProps). Один нюанс: внутри каждого чата у нас не плоская лента, а дерево — parentMessageId на каждом узле, продолжать можно от любой точки. Полезно при откате к моменту до неудачного решения модели - не «удаляем», а уходим в новую ветку от того же родителя. Ваш Telegram-топик отлично решает изоляцию доменов.
Второе - fallback вместо retry. Полностью так же. Retry на недетерминированной системе странная идея, если вдуматься: тот же промпт, тот же state, та же температура почему второй раз должно стать лучше? У нас .OnException().Continued().To("llm://...?systemPromptRef=#simplified"), упрощённый сломался — в очередь на человека.
Третье observability цепочки. Больно знакомо. весь цикл (промпт, вызов, результат, действие) пишется в MessageProps + ToolAuditProps, плюс wire-tap в Kafka — через год можно поднять «а что мы 15 апреля чего наотвечали».
и про «недетерминированность как операционный риск только в одних доменах, и архитектурно надо закладывать сразу» — лучшая формулировка, я бы её в эпиграф вынес. В EdTech контент косячит — переписали и забыли. В финансах ту же ошибку трактуют как fraud, в медицине — как смертельный исход. Поэтому в статье 10 паттернов, а не «вот правильный способ делать AI».
Почему сложные графы быстрее — не маркетинг, а механика
Три конкретных причины:
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.
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» тут не получилось бы.
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:
Удалили объект — каскадом удалились все его свойства. Удалили 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-фасеты из любого языка:
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().
Дополню про концентрацию вендора — она тут слабее, чем кажется. Ядро, движок, коннекторы и рантайм открыты: заброшенность лечится форком, supply chain — аудитом (а в век LLM прошерстить всё дерево это вечер, а не неделя). Pro-слой закрыт бинарём, но, во-первых, раздаётся бесплатно и без лицензионного сервера на всей линии 3.x — «рубильника» у вендора нет; во-вторых, компаниям, которым нужен исходник, я отдаю его по запросу. Так что и тут это не lock-in, а обычный риск «поставщик может остановиться» — с открытым ядром и доступным по запросу Pro это не тупик, а форк.
Согласен с самим принципом: изоляция отказов реальна, и если бы обновление шины насильно тянуло за собой перевыпуск хранилища — это был бы антипаттерн. Но здесь смешаны две ортогональные вещи: изоляция в рантайме и связность версий на сборке.
В рантайме изоляция никуда не делась. Шина, БД и рантайм — по-прежнему отдельные процессы (и отдельные NuGet-пакеты). Регрессия в шине не роняет хранилище: откатываете один пакет/образ шины, база живёт дальше. Ровно ваш сценарий «слегла шина — база живёт» работает как есть, откат — один слой.
«Единый бамп» — это рекомендация, а не жёсткая связность. Пакеты версионируются независимо и совместимы по API внутри 3.x:
redb.Route3.3.0 спокойно едет с коннектором 3.2.x — просто без свежих фиксов конкурентности. То есть «чтобы починить шину, придётся перевыпускать хранилище» фактически не так — бампаете только тот пакет, который трогали. Единая линия версий нужна тем, кому удобно ехать всем стеком разом, а не обязаловка.А главный плюс синхронного выпуска — он гоняется тестами комплексно. Вся линия 3.3.0 проходит сквозную матрицу: БД, шина, коннекторы, рантайм проверяются в связке, на одном прогоне. В зоопарке такого прогона нет в принципе — каждый вендор тестирует себя у себя, а стык между ними (та самая интеграция версий) не тестирует никто, кроме вас, в проде. Синхронный релиз ровно эту дыру и закрывает: совместимость слоёв — гарантия линии, а не ваша ночная проверка.
И про «клей, который чинишь по ночам». В зоопарке по ночам обычно чинится не вендорский компонент, а именно клей между ними — адаптеры, контракты сериализации, мосты транзакций. Это ваш код: без вендора, без тестов, без SemVer. Здесь этот клей — тестированные версионированные примитивы одной линии с общей CI-матрицей. Регрессию ловит один прогон по всему стеку, а не «а совместим ли драйвер vX с шиной vY».
И ещё про «болеют по отдельности». Так они и лечатся по отдельности — разным стеком, разными людьми. Kafka чинит один спец, Postgres — DBA, Redis — третий, шину — четвёртый. Изоляция отказов оплачивается фрагментацией экспертизы: держать надо не один навык, а зоопарк компетенций, и в 3 ночи нужный человек не всегда на смене. Единая экосистема — это один стек знаний на всё, и правит его один человек, а не эстафета из четырёх дежурных. ну и вся эта история открытый исходный код.
Где вы правы по-настоящему: реальный компромисс не в изоляции отказов (она сохранена), а в концентрации вендора. Один поставщик на весь стек — это единый источник и багов, и рисков (заброшенность, вектор supply chain). Это честная цена за отсутствие интеграционного клея и за один стек компетенций, и держать её в голове стоит. Но это уже другой аргумент, чем «синхронный бамп ломает откат».
но опят же открытый исходный код в век нейронок прошерстить не составляет труда.
точно, это русский фольклор, не переводимая игра слов
task -> tsak = ЦАК
колокольчик
- у apach camel - apach karaf = кувшин.
Мне нравится этот фильм. "Кин-дза-дза" Георгия Данелии.
Глубокий философский смысл несет он в себе.
Тут небольшая путаница — redb.dll это не замена sqlite3.dll и не «другой SQLite», а загружаемое расширение SQLite (.load / sqlite3_load_extension). Оно не заменяет движок, а грузится поверх обычного SQLite: при загрузке через sqlite3ext.h резолвит API хоста и добавляет в базу функции redb (get_object_json, pvt_*-компилятор запросов, soft-delete, view прав).
Поэтому по пунктам:
— Быстрее? Некорректное сравнение: redb.dll использует ваш sqlite3 как есть и в его скорости ничего не меняет. Ценность не в скорости SQLite, а в том, что он превращает SQLite в типизированное объектное хранилище (серверный материализатор + компилятор запросов внутри БД — ровно то, что у Free-тира на Postgres/MSSql живёт как PL/pgSQL).
— Совместимо/взаимозаменяемо? Совместимо с SQLite (грузится в любой sqlite3 ≥ 3.44 — Microsoft.Data.Sqlite, python sqlite3, sqlite3 CLI), но не взаимозаменяемо: сам sqlite3.dll по-прежнему нужен, redb.dll к нему добавляется, а не вместо.
Если совсем коротко: sqlite3.dll — это движок, redb.dll — плагин к нему.
смотри на redb.ru
ну и если честно мне тоже не нравятся лозунги
- всё равно его не брошу потому что он хороший
- этого не может быть, потому что не может быть
- миграции это важный и сложный процесс
- риски потому что риски
здесь да, теряется из проекта что то такое большое сложное и важное, это БД с EF
если кто спросит а сколько у тебя таблиц в БД у тебя же сотни сущностей, то наверное да, ответить нечего..., да нисколько.
попробую тоже резюмировать
я не призываю отказываться от EF и даппера и нет проблем,
я просто показываю что это вполне рабочий вариант и как оно помогает достичь конечной цели бизнеса, быстрее и качественнее, а не строить БД ради БД
PS в моих словах нет критики или сарказма, наверное надо понимать что я вот это построил имея некоторые так сказать знания работы(и внутренностей) EF и прочих Hibernate
вся эта история прекрасно вяжется с
redb.route - альтернатива MassTransit и прочих басов
redb.tsak - альтернатива karaf и прочих роуте контейнеров
и всё-же я ожидал не только критику, но странно почему нет вопросов, обычно если туда погрузится, тема не простая, и вопросов должно возникнуть масса...
деревья с linq, самый смак тама в них, есть и полиморфные деревья
самое интересное я знаю во сколько обошлась компании экономя средств на проекте на большом, реально даже не один лям.
спасибо, таки да и без нейронки общаемся, без заученных книжных оборотов.
почему я позиционирую как альтернативу 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, но там много фич в планах
я просто всё не успеваю физически, на подходе идентити сервер, но он пока на тестах на прод не несли
днем основная работа, никто не отменял
не много не согласен
похож на 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 делалось) у меня нет, не коллекционирую, показываю вот прям что счас у меня есть (без всяких яких).
если кому интересно могу нарезать с прода много картинок, с замерами всякими.
но есть нюанс, с нейронкой если ей всё грамотно показать, проекты делаются в разы еще быстрее, потому что работа только с бизнес сущностями и бизнес процессами.
Granulex Спасибо за развёрнутый ответ — взял в работу. В
3.1.1(CHANGELOG уже обновлён) добавил per-message аудит-поля наMessageProps, проставляются движком на каждый persist:Temperature,MaxTokens,TopP— эффективные сэмплинг-параметры (override запроса → дефолт фабрики), на assistant-строках.PromptTemplateName+PromptTemplateVersion— на всех строках прогона, если caller передал managed template.ToolSetHash— SHA-256 канонического набора{name, description, InputSchema}(отсортирован по имени), на assistant-строках. Меняется набор/схема инструмента — меняется хеш.ProviderSystemFingerprint—system_fingerprintиз ответа провайдера (OpenAI/xAI/Together эхо-возвращают; Anthropic-compat / Gemini-compat / Ollama чащеnull).Про закрытые модели честно: фингерпринта от Anthropic нет → bit-exact replay невозможен в принципе, фиксируем (model_id, params, tool_set_hash, prompt_template) и помечаем как best-effort. Для жёсткого compliance — self-hosted (Ollama / vLLM / llama.cpp), там воспроизводимость честная.
Миграции не было — REDB схему props подхватывает автоматически, просто добавил поля в класс.
Детали и code-paths: CHANGELOG.md → раздел 3.1.1, per-message audit fields on
MessagePropsИнтересный вопрос, здесь-то почти никак...
Что закрывается из коробки. Каждое сообщение в
MessagePropsнесётprovider_id,model_id,stop_reason, токены и полный контент (text + tool_use + tool_result, без потерь). Промпты живут в отдельном стореPromptTemplatePropsс(name, version, body)— то есть «промпт v7 от 14 апреля» это конкретный объект в Postgres, который нельзя перезаписать незаметно.Что НЕ закрыто, но важно для аудитора. На уровне per-message сейчас не пишется
temperature/maxTokensи нет явной FK на(prompt_name, prompt_version). Это известная дыра — то самое «нащупали разрыв».План — добавить в
MessagePropsполяTemperature,PromptTemplateName,PromptTemplateVersion,ToolSetHash(чтобы зафиксировать, какой набор tool'ов был активен на момент вызова). И вот тут как раз тот случай, где REDB снимает обычную боль: добавляешь поля в C#-класс props — и всё, никаких миграций, alter table, версий схемы и прочей светотени. Стор сам подхватывает новые атрибуты. То есть это не «архитектурное решение на спринт», а реально пара коммитов. Возьму в работу.Что закрыть нельзя в принципе — drift на стороне провайдера. Anthropic может молча пере-зарелизить
claude-sonnet-4-5, и вы про это узнаете только по поведению. Единственный честный ответ для compliance-доменов — либо self-hosted (vLLM / llama.cpp / Ollama, у нас есть алиасы), либо явный pinning через провайдеров, у которых это есть в API (xAI, OpenAI черезsystem_fingerprint, Cohere). У Anthropic такого пока нет, и это плохо для финансов/медицины. Я закладываю это как явную лимитацию в архитектурный контракт, а не пытаюсь магией скрыть.у нас то же, через
?conversation=header: id из хедера, история живёт в Postgres (ConversationProps). Один нюанс: внутри каждого чата у нас не плоская лента, а дерево —parentMessageIdна каждом узле, продолжать можно от любой точки. Полезно при откате к моменту до неудачного решения модели - не «удаляем», а уходим в новую ветку от того же родителя. Ваш Telegram-топик отлично решает изоляцию доменов.Второе - fallback вместо retry. Полностью так же. Retry на недетерминированной системе странная идея, если вдуматься: тот же промпт, тот же state, та же температура почему второй раз должно стать лучше? У нас
.OnException().Continued().To("llm://...?systemPromptRef=#simplified"), упрощённый сломался — в очередь на человека.Третье observability цепочки. Больно знакомо. весь цикл (промпт, вызов, результат, действие) пишется в
MessageProps+ToolAuditProps, плюс wire-tap в Kafka — через год можно поднять «а что мы 15 апреля чего наотвечали».и про «недетерминированность как операционный риск только в одних доменах, и архитектурно надо закладывать сразу» — лучшая формулировка, я бы её в эпиграф вынес. В EdTech контент косячит — переписали и забыли. В финансах ту же ошибку трактуют как fraud, в медицине — как смертельный исход. Поэтому в статье 10 паттернов, а не «вот правильный способ делать AI».
Почему сложные графы быстрее — не маркетинг, а механика
Три конкретных причины:
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.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:
Что генерируется на PG.Pro:
Заметные детали:
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» тут не получилось бы.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-типом:Каждое свойство пишется в свою типизированную колонку.
WHERELong > 1000идёт по обычному B-tree,WHEREDateTimeOffset >= '2026-01-01'— то же самое. НикакогоCAST(value AS bigint)поверх text-колонки, как в classic EAV.2. FK везде, не только индексы — целостность на уровне БД
Это, наверное, самое важное и редко встречающееся в EAV-системах. Реальный DDL из redbPostgre.sql:
Удалили объект — каскадом удалились все его свойства. Удалили scheme — каскадом всё, что на ней висит. Удалили scheme-родителя — children почистились (
_id_parent →structures.id ON DELETE CASCADE). ПоссылочныйRedbObject<T>—_Objectтоже FK. В classic EAV ничего такого нет, потому что там нет понятия «структура» как first-class сущности.И всё это покрыто индексами:
То есть план запроса с
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:DBA, BI-инженер, аудитор — открыл psql, получил полное описание схемы без единой строки .NET-кода. C# вообще не обязателен — фасеты и схемы можно строить из Python / Node / Go / Java / raw SQL. Schema-as-data, без хождения в приложение.
4. Теперь про DBA-инструменты — четыре конкретных ответа
4.1.
get_object_json(id, depth)— встроенная SQL-функция в обоих диалектах:Полный граф объекта как JSON: вложенные структуры, массивы, словари, ссылки. «Дай мне этот заказ как есть» — одна строка SQL.
4.2. Полиглот-API. Free-движок строит SQL внутри БД, фильтры можно слать как JSON-фасеты из любого языка:
Python (psycopg), Node (pg), Go (pgx), Java (JDBC), raw psql — .NET не нужен.
4.3.
pg_stat_statementsработает. Pro-движок генерирует параметризованный SQL с$1..$Nplaceholders — стабильная форма, статистика агрегируется нормально. 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().почему в проде не юзаем MassTransit
потому что юзаем это https://github.com/redbase-app/redb-route