REDB SQL
REDB SQL

В предыдущей статье я разобрал что умеет REDB на практике: code-first схемы, LoadAsync без Include, LINQ с оконными функциями, деревья, redb.Route и redb.Tsak. Если не читали — начните с неё, она даёт общую картину.

Теперь начинаю цикл «REDB изнутри» — серия статей про устройство хранилища: как хранятся объекты и свойства, как работают схемы и кэш структур, как LINQ превращается в SQL, как устроены деревья, права, оконные функции. Всё с реальным SQL под капотом.

Первая статья цикла — про саму базу данных. Без понимания схемы таблиц всё остальное будет висеть в воздухе.

Вся магия REDB делается в 13 таблицах. Никакого JSON-блоба, никакого NVARCHAR(MAX) для всего подряд — каждый тип данных в своей типизированной колонке. Разберём как это работает и почему это не классический EAV.


Почему не «EAV»

Прежде чем смотреть на таблицы — важное терминологическое замечание.

EAV (Entity–Attribute–Value) — это паттерн хранения данных, при котором строки таблицы выглядят так:

object_id | attribute_name | value
----------|----------------|-------
42        | "FirstName"    | "Alice"
42        | "Age"          | "28"
42        | "Salary"       | "85000"

Всё в одной таблице, типы стёрты, имена атрибутов — просто строки. Любой JOIN адский. Запрос «найди сотрудников с зарплатой > 80000» — это самоджойн или pivot. EXPLAIN выдаёт Seq Scan размером с диссертацию.

REDB использует другую схему. В values — не имена строками, а ссылки на structures (_id_structure bigint NOT NULL). Тип значения известен в момент записи. Для каждого C#-типа — своя колонка:

_String         text
_Long           bigint
_Guid           uuid
_Double         float
_DateTimeOffset timestamptz
_Boolean        boolean
_ByteArray      bytea
_Numeric        NUMERIC(38, 18)
_ListItem       bigint   -- FK → _list_items
_Object         bigint   -- FK → _objects (ссылка на объект)

Это ближе к RTTI (Run-Time Type Information): тип поля известен из structures.id_type, и значение кладётся в правильную колонку. При чтении — CASE WHEN db_type = 'Long' THEN Long WHEN dbtype = 'String' THEN _String ... — один проход по строкам, никаких самоджойнов.

Разница принципиальная:

EAV

REDB (_values)

Тип поля

строка в attribute_name

FK → structures → types

Хранение значения

всё в одну value text колонку

в типизированную колонку (_Long_String, ...)

Фильтр Salary > 80000

WHERE attribute_name='Salary' AND value::int > 80000

WHERE idstructure = @struct_id AND _Long > 80000

Индекс по значению

по строке, cast на лету

по типизированной колонке (partial index WHERE _Long IS NOT NULL)

Поддержка массивов

дополнительная таблица / JSON

arrayparent_id + arrayindex в той же таблице


Обзор таблиц

Полная схема — 13 таблиц + 2 служебных, которые живут в отдельных SQL-файлах.

_types          — справочник типов (~37 записей)
_schemes        — схемы (C# классы → строки в БД)
_structures     — поля схем (с поддержкой вложенности и коллекций)
_objects        — объекты (строки данных, дерево через self-FK)
_values         — значения полей (RTTI-хранение + коллекции)
_lists          — справочники
_list_items     — элементы справочников
_users          — пользователи
_roles          — роли
_users_roles    — M2M: пользователь ↔ роль
_permissions    — права на объекты (с наследованием по дереву)
_links          — M2M связи между объектами
_functions      — хранимые выражения для схем
_dependencies   — межсхемные зависимости
---
_scheme_metadata_cache  — денормализованный кэш structures+types
_migrations             — история миграций Props

Разберём послойно.


Слой типов: _types

CREATE TABLE _types (
    _id     bigint NOT NULL,
    _name   text NOT NULL UNIQUE,
    _db_type text NULL,   -- колонка в _values: 'Long', 'String', 'Guid', ...
    _type   text NULL,    -- C#-тип: 'long', 'string', 'Guid', ...
    CONSTRAINT PK__types PRIMARY KEY (_id)
);

Все ID — отрицательные константы из диапазона около Long.MinValue. Примеры:

_id

_name

dbtype

C# тип

-9223372036854775709

Boolean

Boolean

bool

-9223372036854775708

DateTime

DateTimeOffset

DateTime

-9223372036854775704

Long

Long

long

-9223372036854775700

String

String

string

-9223372036854775695

Decimal

Numeric

decimal

-9223372036854775675

Class

вложенный класс (маркер)

-9223372036854775668

Array

T[] / List<T> (маркер)

-9223372036854775667

Dictionary

Dictionary<K,V> (маркер)

Числа выглядят страшно — это просто константы, гарантирующие, что системные типы никогда не пересекутся с пользовательскими данными. У Class/Array/Dictionary нет своей колонки в _values — это маркерные типы, значения хранятся в дочерних строках.

Всего ~37 типов: числовые (Int/Short/Byte/Float → хранятся в Long/Double), строковые (Email/Url/Phone → String), временны́е (DateOnly/TimeOnly/TimeSpan), географические (Latitude/Longitude), файловые (FilePath/MimeType), Enum/EnumInt, коллекционные (Array/Dictionary/JsonDocument/XDocument).


Слой схем: schemes + structures

CREATE TABLE _schemes (
    _id             bigint NOT NULL,
    _id_parent      bigint NULL,          -- для пространств имён
    _name           text NOT NULL UNIQUE, -- полное имя: 'MyApp.Models.EmployeeProps'
    _alias          text NULL,
    _structure_hash uuid NULL,            -- hash полей для быстрой проверки изменений
    _type           bigint NOT NULL       -- FK → _types (Class по умолчанию)
);

CREATE TABLE _structures (
    _id             bigint NOT NULL,
    _id_parent      bigint NULL,     -- вложенный Props-класс
    _id_scheme      bigint NOT NULL, -- FK → _schemes
    _id_type        bigint NOT NULL, -- FK → _types
    _id_list        bigint NULL,     -- FK → _lists (для ListItem-полей)
    _name           text NOT NULL,   -- имя поля (C# property name)
    _alias          text NULL,
    _order          bigint NULL,
    _collection_type bigint NULL,    -- NULL=скаляр, Array_ID / Dictionary_ID
    _key_type       bigint NULL,     -- тип ключа для Dictionary<K,V>
    _readonly       boolean NULL,
    _allow_not_null boolean NULL,
    _is_compress    boolean NULL,
    _store_null     boolean NULL,
    _default_value  bytea NULL
);

Как схемы связаны с C#

Когда вы пишете:

[RedbScheme("Сотрудник")]
public class EmployeeProps
{
    public string FirstName { get; set; }
    public int Age { get; set; }
    public decimal Salary { get; set; }
    public string[] Skills { get; set; }
    public Address HomeAddress { get; set; }   // вложенный класс
}

И вызываете await redb.SyncSchemeAsync<EmployeeProps>(), система:

  1. Создаёт строку в _schemes с именем MyApp.EmployeeProps

  2. Создаёт строки в _structures для каждого поля

  3. Для Skills (массив) — collectiontype = Array_ID

  4. Для HomeAddress (вложенный класс) — idtype = Class_ID, и рекурсивно создаёт дочерние structures с id_parent = HomeAddress._structure_id

Триггер на уровне БД проверяет имена полей: запрещены системные (_idnamedate_create и т.д.), C#-зарезервированные (classintstring...), имена не должны начинаться с цифры.


Слой объектов: _objects

CREATE TABLE _objects (
    _id             bigint NOT NULL,
    _id_parent      bigint NULL,          -- родитель в дереве (self-FK)
    _id_scheme      bigint NOT NULL,      -- FK → _schemes
    _id_owner       bigint NOT NULL,      -- FK → _users
    _id_who_change  bigint NOT NULL,      -- FK → _users
    _date_create    timestamptz NOT NULL,
    _date_modify    timestamptz NOT NULL,
    _date_begin     timestamptz NULL,
    _date_complete  timestamptz NULL,
    _key            bigint NULL,
    _name           text NULL,
    _note           text NULL,
    _hash           uuid NULL,
    -- value-колонки для RedbPrimitive<T>
    _value_long     bigint NULL,
    _value_string   text NULL,
    _value_guid     uuid NULL,
    _value_bool     boolean NULL,
    _value_double   float NULL,
    _value_numeric  NUMERIC(38, 18) NULL,
    _value_datetime timestamptz NULL,
    _value_bytes    bytea NULL
);

Несколько нетривиальных вещей:

Дерево через idparent — ON DELETE CASCADE. Удаление объекта автоматически удаляет всё поддерево, любой глубины. Это основная структура для организации данных в REDB: разделы, категории, папки, оргструктура — всё одним механизмом.

value* колонки — для RedbPrimitive<T>. Когда объект сам является примитивным значением (например, RedbObject<long> для счётчика или RedbObject<string> для токена), значение хранится прямо в строке объекта, без _values. Это 8 колонок — по одной на каждый db-тип.

Мягкое удаление — специальная схема @@__deleted (_id = -10). Функция mark_for_deletion() рекурсивно находит все потомки через CTE и атомарно перемещает их под TRASH-контейнер (_id_scheme = -10). Физическое удаление — отдельная purge_trash() батч-операция.


Слой значений: _values

Это самая насыщенная таблица. Здесь живут все значения всех полей всех объектов.

CREATE TABLE _values (
    _id              bigint NOT NULL,
    _id_structure    bigint NOT NULL,   -- FK → _structures
    _id_object       bigint NOT NULL,   -- FK → _objects
    -- типизированные value-колонки
    _String          text NULL,
    _Long            bigint NULL,
    _Guid            uuid NULL,
    _Double          float NULL,
    _DateTimeOffset  timestamptz NULL,
    _Boolean         boolean NULL,
    _ByteArray       bytea NULL,
    _Numeric         NUMERIC(38, 18) NULL,
    _ListItem        bigint NULL,       -- FK → _list_items
    _Object          bigint NULL,       -- FK → _objects (ссылка на другой объект)
    -- поля для реляционного хранения коллекций
    _array_parent_id bigint NULL,       -- FK → _values (родительский элемент)
    _array_index     text NULL          -- '0','1','2' для массивов, ключ для словарей
);

Как хранятся коллекции

Это самое нетривиальное место. Коллекции хранятся реляционно прямо в _values, без дополнительных таблиц:

Скалярное поле (например, Age = 28):

_id_structure=struct_Age  _id_object=42  _Long=28  _array_parent_id=NULL  _array_index=NULL

Массив строк (Skills = ["C#", "SQL", "React"]):

-- маркер: "массив присутствует" (без него свойство = null, а не [])
_id=100  _id_structure=struct_Skills  _id_object=42  _array_index=NULL  _array_parent_id=NULL

-- элементы с индексами
_id=101  _id_structure=struct_Skills  _id_object=42  _String="C#"     _array_index='0'  _array_parent_id=100
_id=102  _id_structure=struct_Skills  _id_object=42  _String="SQL"    _array_index='1'  _array_parent_id=100
_id=103  _id_structure=struct_Skills  _id_object=42  _String="React"  _array_index='2'  _array_parent_id=100

Словарь (PhoneDir = {"home": "+7...", "work": "+7..."}):

-- маркер словаря
_id=200  _id_structure=struct_PhoneDir  _id_object=42  _array_index=NULL  _array_parent_id=NULL

-- записи с строковыми ключами
_id=201  _id_structure=struct_PhoneDir  _id_object=42  _String="+7 999..."  _array_index='home'  _array_parent_id=200
_id=202  _id_structure=struct_PhoneDir  _id_object=42  _String="+7 495..."  _array_index='work'  _array_parent_id=200

Вложенный класс (Address.City = "Москва") — дочерние поля Address имеют idparent в structures, а их values имеют arrayparent_id указывающий на маркер-строку Address-поля.

Три уникальных индекса в _values обеспечивают консистентность всех этих случаев:

  • UIX__values__structure_object — WHERE arrayindex IS NULL AND arrayparent_id IS NULL

  • UIX__values__structure_object_parent — WHERE arrayindex IS NULL AND arrayparent_id IS NOT NULL

  • UIX__values__structure_object_array_index — WHERE arrayindex IS NOT NULL


Права: _permissions

CREATE TABLE _permissions (
    _id      bigint NOT NULL,
    _id_role bigint NULL,   -- XOR с _id_user (CHECK constraint)
    _id_user bigint NULL,
    _id_ref  bigint NOT NULL,  -- 0 = глобальные права, иначе ID объекта
    _select  boolean NULL,
    _insert  boolean NULL,
    _update  boolean NULL,
    _delete  boolean NULL
);

Права наследуются по дереву объектов — рекурсивный CTE идёт вверх до 50 уровней в поисках ближайшего предка с явно заданными правами. idref = 0 — глобальный fallback (права на всё). Приоритет: user > role, конкретный объект > глобальные.

Есть автотриггер: при создании дочернего объекта — если у родителя ещё нет собственных прав — триггер автоматически создаёт ему запись, копируя права ближайшего предка. Это сокращает глубину рекурсии при каждом чтении прав.


schememetadata_cache — зачем нужен кэш

Каждый запрос к данным требует знать: для объекта с idscheme = X, какие поля (_structures) существуют и какого типа? Это JOIN structures → types, который повторяется на каждом обращении.

Решение — денормализованная таблица schememetadata_cache. Она содержит все колонки structures + typenamedb_typetype_semanticcollection_type_name из _types — плоской строкой, без джойна.

CREATE TABLE _scheme_metadata_cache (
    _scheme_id       bigint NOT NULL,
    _structure_id    bigint NOT NULL,
    _parent_structure_id bigint,
    _name            text NOT NULL,
    type_name        text NOT NULL,  -- 'Long', 'String', 'Guid', ...
    db_type          text NOT NULL,  -- 'Long', 'String', 'Guid', ...
    type_semantic    text NOT NULL,  -- 'Object', '_RObject', 'Array', ...
    _collection_type bigint,
    collection_type_name text,
    _key_type        bigint,
    key_type_name    text,
    -- ... остальные атрибуты _structures
);

Кэш автоматически обновляется триггером при изменении schemes.structure_hash. При запросе к схеме, чьего кэша нет — вызывается sync_metadata_cache_for_scheme(scheme_id). Вся функция build_hierarchical_properties_optimized() (которая собирает JSON объекта) работает исключительно через этот кэш.


Два SQL-запроса: заглянуть внутрь объекта

Запрос 1 — без кэша (прямые join)

Для объекта с заданным ID — плоский список всех полей с именем схемы, именем поля, типом и значением.

-- Postgres
SELECT
    s._name                                          AS scheme_name,
    st._name                                         AS field_name,
    t._name                                          AS type_name,
    CASE t._db_type
        WHEN 'String'        THEN v._String
        WHEN 'Long'          THEN v._Long::text
        WHEN 'Guid'          THEN v._Guid::text
        WHEN 'Double'        THEN v._Double::text
        WHEN 'Boolean'       THEN v._Boolean::text
        WHEN 'DateTimeOffset' THEN v._DateTimeOffset::text
        WHEN 'Numeric'       THEN v._Numeric::text
        WHEN 'ListItem'      THEN v._ListItem::text
        WHEN 'Object'        THEN v._Object::text
        WHEN 'ByteArray'     THEN encode(v._ByteArray, 'base64')
        ELSE                      NULL
    END                                              AS value_text,
    CASE
        WHEN v._array_parent_id IS NULL
         AND v._array_index  IS NULL
         AND t._name IN ('Array','Dictionary','Class') THEN 'collection_marker'
        WHEN v._array_parent_id IS NULL
         AND v._array_index  IS NULL                  THEN 'scalar'
        WHEN v._array_parent_id IS NOT NULL           THEN 'element[' || v._array_index || ']'
        ELSE                                               'scalar'
    END                                              AS slot,
    v._array_index,
    v._array_parent_id
FROM _values      v
JOIN _structures  st ON st._id = v._id_structure
JOIN _schemes     s  ON s._id  = st._id_scheme
JOIN _types       t  ON t._id  = st._id_type
WHERE v._id_object = :object_id   -- подставить нужный ID
ORDER BY st._order NULLS LAST, v._array_index NULLS FIRST;
-- MS SQL (те же данные, синтаксис параметра @object_id)
SELECT
    s._name                                       AS scheme_name,
    st._name                                      AS field_name,
    t._name                                       AS type_name,
    CASE t._db_type
        WHEN 'String'         THEN v._String
        WHEN 'Long'           THEN CAST(v._Long AS nvarchar(MAX))
        WHEN 'Guid'           THEN CAST(v._Guid AS nvarchar(MAX))
        WHEN 'Double'         THEN CAST(v._Double AS nvarchar(MAX))
        WHEN 'Boolean'        THEN CASE v._Boolean WHEN 1 THEN 'true' WHEN 0 THEN 'false' END
        WHEN 'DateTimeOffset' THEN CAST(v._DateTimeOffset AS nvarchar(MAX))
        WHEN 'Numeric'        THEN CAST(v._Numeric AS nvarchar(MAX))
        WHEN 'ListItem'       THEN CAST(v._ListItem AS nvarchar(MAX))
        WHEN 'Object'         THEN CAST(v._Object AS nvarchar(MAX))
        WHEN 'ByteArray'      THEN CAST('' AS nvarchar(MAX))  -- base64 через приложение
        ELSE                       NULL
    END                                           AS value_text,
    CASE
        WHEN v._array_parent_id IS NULL AND v._array_index IS NULL THEN 'scalar'
        WHEN v._array_parent_id IS NOT NULL THEN 'element[' + ISNULL(v._array_index,'') + ']'
        ELSE 'scalar'
    END                                           AS slot,
    v._array_index,
    v._array_parent_id
FROM [dbo].[_values]      v
JOIN [dbo].[_structures]  st ON st._id = v._id_structure
JOIN [dbo].[_schemes]     s  ON s._id  = st._id_scheme
JOIN [dbo].[_types]       t  ON t._id  = st._id_type
WHERE v._id_object = @object_id
ORDER BY st._order, v._array_index;

Этот запрос — диагностический. Он даёт полную картину: что именно хранится для объекта, в каком слоте, с каким типом. Маркерные строки массивов (_array_parent_id IS NULL AND arrayindex IS NULL) тоже видны — именно их наличие/отсутствие отличает null от [].


Запрос 2 — через schememetadata_cache

Это то, что в реальности делает движок при чтении. Кэш уже содержит денормализованные type_name/db_type, поэтому JOIN с types и structures не нужен:

-- Postgres (через _scheme_metadata_cache)
SELECT
    c._scheme_id                                      AS scheme_id,
    c._name                                           AS field_name,
    c.type_name                                       AS type_name,
    c.db_type                                         AS db_type,
    c.collection_type_name                            AS collection_type,
    CASE c.db_type
        WHEN 'String'         THEN v._String
        WHEN 'Long'           THEN v._Long::text
        WHEN 'Guid'           THEN v._Guid::text
        WHEN 'Double'         THEN v._Double::text
        WHEN 'Boolean'        THEN v._Boolean::text
        WHEN 'DateTimeOffset' THEN v._DateTimeOffset::text
        WHEN 'Numeric'        THEN v._Numeric::text
        WHEN 'ListItem'       THEN v._ListItem::text
        WHEN 'Object'         THEN v._Object::text
        WHEN 'ByteArray'      THEN encode(v._ByteArray, 'base64')
        ELSE                       NULL
    END                                               AS value_text,
    v._array_index,
    v._array_parent_id,
    c._order
FROM _values                  v
JOIN _scheme_metadata_cache   c ON c._structure_id = v._id_structure
JOIN _objects                 o ON o._id = v._id_object
WHERE v._id_object = :object_id
ORDER BY c._order NULLS LAST, v._array_index NULLS FIRST;
-- MS SQL (через _scheme_metadata_cache)
SELECT
    c.[_scheme_id]                                     AS scheme_id,
    c.[_name]                                          AS field_name,
    c.[type_name]                                      AS type_name,
    c.[db_type]                                        AS db_type,
    c.[collection_type_name]                           AS collection_type,
    CASE c.[db_type]
        WHEN 'String'         THEN v._String
        WHEN 'Long'           THEN CAST(v._Long AS nvarchar(MAX))
        WHEN 'Guid'           THEN CAST(v._Guid AS nvarchar(MAX))
        WHEN 'Double'         THEN CAST(v._Double AS nvarchar(MAX))
        WHEN 'Boolean'        THEN CASE v._Boolean WHEN 1 THEN 'true' WHEN 0 THEN 'false' END
        WHEN 'DateTimeOffset' THEN CAST(v._DateTimeOffset AS nvarchar(MAX))
        WHEN 'Numeric'        THEN CAST(v._Numeric AS nvarchar(MAX))
        WHEN 'ListItem'       THEN CAST(v._ListItem AS nvarchar(MAX))
        WHEN 'Object'         THEN CAST(v._Object AS nvarchar(MAX))
        WHEN 'ByteArray'      THEN N'<binary>'
        ELSE                       NULL
    END                                                AS value_text,
    v._array_index,
    v._array_parent_id,
    c.[_order]
FROM [dbo].[_values]                  v
JOIN [dbo].[_scheme_metadata_cache]   c ON c.[_structure_id] = v.[_id_structure]
JOIN [dbo].[_objects]                 o ON o.[_id] = v.[_id_object]
WHERE v.[_id_object] = @object_id
ORDER BY c.[_order], v.[_array_index];

Чем второй лучше первого в продакшне:

  1. Нет JOIN на structures и types — два тяжёлых джойна убраны

  2. Кэш уже отсортирован по order + проиндексирован по structure_id

  3. Сам кэш заполняется один раз при изменении схемы (_structure_hash), а не при каждом запросе

  4. Функция build_hierarchical_properties_optimized идёт ещё дальше: она загружает все values объекта в массив values[] одним запросом и потом работает только с памятью через unnest() — ни одного повторного обращения к таблице


Как это связано с C#

Для полноты картины — что происходит за каждым вызовом. Полные примеры кода были в предыдущей статье, здесь только связка с таблицами:

[RedbScheme("Сотрудник")]
public class EmployeeProps
{
    public string FirstName { get; set; }
    public string LastName  { get; set; }
    public int    Age       { get; set; }
    public decimal Salary   { get; set; }
    public string[] Skills  { get; set; }
}

// InitializeAsync сканирует сборку → создаёт строки в _schemes + _structures
await redb.InitializeAsync(typeof(EmployeeProps).Assembly);

// SaveAsync → строка в _objects + строки значений в _values
var employee = new RedbObject<EmployeeProps>
{
    name  = "Алиса Иванова",
    Props = new EmployeeProps
    {
        FirstName = "Алиса",
        LastName  = "Иванова",
        Age       = 28,
        Salary    = 120_000m,
        Skills    = ["C#", "PostgreSQL", "Redis"]  // → маркер + 3 строки в _values
    }
};
long id = await redb.SaveAsync(employee);

// LoadAsync → SELECT _objects + SELECT _values WHERE _id_object = id
// → рекурсивная сборка через build_hierarchical_properties_optimized
var loaded = await redb.LoadAsync<EmployeeProps>(id);

SaveAsync обращается к schememetadata_cache чтобы узнать структуру полей, генерирует ключи через sequence global_identity, кладёт строку в objects и строки значений в valuesLoadAsync читает objects, затем все values одним SELECT в массив values[], рекурсивно собирает иерархию через buildhierarchical_properties_optimized — исключительно через unnest() в памяти, без повторных обращений к таблице.


Почему именно так

Несколько решений, которые не очевидны с первого взгляда:

Отрицательные константы для системных ID — гарантия, что пользовательские данные (через global_identity, который стартует с 1_000_000) никогда не пересекутся с системными типами/схемами/пользователями.

structurehash в schemes — позволяет не пересчитывать кэш при каждом запросе. Хеш меняется только при изменении полей схемы. Триггер на schemes инвалидирует кэш точечно, без перестройки всего.

Маркерные строки в values — нетривиальный выбор. Для коллекций нет отдельной таблицы. Маркер (array_parent_id = NULL, arrayindex = NULL) отличает null (нет маркера) от [] (маркер есть, но дочерних элементов нет). Это позволяет хранить произвольно вложенные структуры без дополнительных таблиц.

_Numeric NUMERIC(38, 18) — сознательный выбор точности для финансовых расчётов. double для денег не подходит из-за накапливаемой ошибки с плавающей точкой.

Postgres vs MSSQL — реальное отличие в cascade delete: в Postgres values имеет ON DELETE CASCADE по FK на structures. В MSSQL это невозможно (ограничение "multiple cascade paths"), поэтому вместо FK — INSTEAD OF DELETE триггер, который делает то же самое вручную.


Цикл «REDB изнутри»

  • Вводная статья — что умеет REDB: LINQ, деревья, redb.Route, redb.Tsak, production cases (уже опубликована)

  • Статья 1 (эта) — структура БД: 13 таблиц, RTTI vs EAV, values, коллекции, scheme_metadata_cache, SQL под капотом

  • Статья 2 — Code-first схемы: как SyncSchemeAsync<T> преобразует C#-класс в строки schemes + structuresstructurehash, автоматический онбординг

  • Статья 3 — CRUD: SaveAsync и LoadAsync: change tracking (TreeDiff), bulk insert через COPY protocol, lazy loading

  • Статья 4 — LINQ-запросы: как Where(x => x.Salary > 80000) превращается в SQL с CASE WHEN idstructure = X THEN _Long END > 80000

  • Статья 5 — Дерево объектов: LoadTreeAsync, GetDescendantsAsync, WhereHasAncestor

  • Статья 6 — Оконные функции: Win.RowNumber()Win.Rank()PartitionBy/OrderBy поверх REDB-объектов


Ссылки

Комментарии (0)