Знакомо?

Реальные проблемы. Реальные решения. Узнаёте себя?

В вашей базе 1000 таблиц. Никто не знает, зачем половина из них.

Каждая сущность порождает 5-10 таблиц. Orders, OrderItems, OrderStatuses, OrderHistory, OrderMetadata... Новый разработчик выходит — месяц на понимание схемы. Изменить одно поле — обновить миграции, DAL, DTO, маппер. Повторить для каждого микросервиса.

С RedBase

Две таблицы. Всё. Ваши бизнес-классы маппятся напрямую. Order с вложенными Items, Customer, History — один C#-класс, одно сохранение, один запрос.

У вас целый проект только для доступа к данным.

Репозитории. Unit of Work. DTO. Профили AutoMapper. Любое изменение затрагивает минимум 5 файлов. Половина кодовой базы существует только для перемещения данных между слоями, которых быть не должно.

С RedBase

Удалите проект DAL. Работайте с бизнес-объектами напрямую. Сохраняйте что есть. Запрашивайте что нужно. Меньше кода = меньше багов.

Ваш джуниор не знает SQL. Но проекту нужна база данных.

Внешние ключи, индексы, нормализация, скрипты миграций... Он просто хочет писать фичи, а не становиться DBA. Но каждый туториал предполагает, что вы знаете, что такое кластерный индекс.

С RedBase

Пишите C#-классы. Сохраняйте объекты. Запрашивайте через LINQ. RedBase позаботится о SQL. Фокусируйтесь на бизнес-логике, а не на устройстве базы.

Миграции сломали продакшн. Опять.

Кто-то добавил NOT NULL колонку без дефолта. Скрипт отката не работает. Два часа ночи, вы вручную правите данные на продакшне, пока PM смотрит.

С RedBase

Никаких миграций. Добавьте свойство в класс — оно просто появится. Удалите свойство — старые данные останутся, ничего не упадёт. Схема эволюционирует вместе с кодом.

Одна сущность. 30 таблиц. 500мс на запрос.

Нужно загрузить полный Order: информация о клиенте, позиции, товары, категории, скидки, доставка, история платежей... SQL-функция на 150 строк. Выполняется полсекунды. А вам нужно запросить тысячи таких.

А теперь умножьте. У вас Orders, Invoices, Projects, Contracts — у каждого своя функция сборки из 30 таблиц. Каждая поддерживается отдельно. Каждая ломается при изменении схемы.

Добавить новое поле? Миграция + обновить функцию сборки + перестроить индексы. Нужно свойство-массив (Tags, Attachments)? Новая таблица + FK + индекс + обновить ВСЕ запросы, которые трогают эту сущность. Каждое изменение каскадом проходит через весь стек.

Традиционный подход
SELECT o.*, c.*, i.*, p.*, cat.*, d.*, s.*, ph.*
FROM Orders o
LEFT JOIN Customers c ON ...
LEFT JOIN OrderItems i ON ...
LEFT JOIN Products p ON ...
LEFT JOIN Categories cat ON ...
LEFT JOIN Discounts d ON ...
LEFT JOIN Shipping s ON ...
LEFT JOIN PaymentHistory ph ON ...
WHERE o.Id = @id
-- 150 строк, 30 JOIN-ов, 500мс
RedBase
var order = await redb.LoadAsync<Order>(id);
// Один поиск по ключу, один дамп данных
// Параллельная сборка объектов в памяти
// Ноль JOIN-ов. 15мс.
Почему другие так не могут?

EF Core — использует JOIN-ы (один огромный запрос) или Split Queries (несколько roundtrip). Последовательная сборка объектов.

Dapper — вы пишете SQL сами. Строите JOIN-ы сами. Собираете объекты вручную.

MongoDB — документы грузятся быстро, но: нет ACID по умолчанию, нет FK-целостности, нет SQL вообще (свой язык запросов), ограниченная поддержка LINQ.

RedBase — реляционная БД (PostgreSQL/SQL Server) с документной загрузкой. Один поиск по ключу, параллельная сборка, полный LINQ, ACID-транзакции. Лучшее из двух миров.

Сохранение одного объекта требует INSERT в 30 таблиц. В правильном порядке.

Ограничения внешних ключей диктуют правильную последовательность. Управление транзакциями. Обработка ошибок для каждого insert. 500 строк кода, чтобы сохранить одну бизнес-сущность. А когда нужно сохранить 1000 за раз? Кошмар производительности.

Традиционный подход
await _transaction.BeginAsync();
try {
    var customerId = await InsertCustomer(order.Customer);
    var orderId = await InsertOrder(order, customerId);
    foreach (var item in order.Items) {
        var productId = await InsertProduct(item.Product);
        await InsertOrderItem(orderId, productId, item);
        // ... ещё 30 вставок в правильном порядке
    }
    await _transaction.CommitAsync();
} catch {
    await _transaction.RollbackAsync();
    throw;
}
RedBase
// Один объект
await redb.SaveAsync(order);

// 1000 объектов — пакетно через BulkInsert
await redb.SaveAsync(orders);

Entity → DTO → ViewModel → Response. Четыре класса для одного и того же.

Сущность базы данных. Бизнес-DTO. Модель ответа API. ViewModel для фронтенда. Профили AutoMapper для каждого направления. Изменить одно поле — обновить четыре класса и три маппинга. Половина ваших багов — баги маппинга.

С RedBase

Один класс. Бизнес-объект = сущность БД = ответ API. Без маппинга. Без AutoMapper. Без DTO-ада. Просто ваша доменная модель.

MVP занимает месяц. Только на схему базы данных.

Стейкхолдеры хотят увидеть что-то. Но сначала нужно спроектировать таблицы, написать миграции, настроить DAL, создать репозитории... К моменту, когда прототип заработал, требования уже изменились.

С RedBase

Утром: определили классы. Вечером: рабочий прототип. Схема растёт вместе с кодом. Выпускайте быстро, итерируйте ещё быстрее.

Клиент хочет одно новое поле. Это занимает неделю.

Тикет DBA. Скрипт миграции. Код-ревью. Деплой на стейджинг. Окно деплоя на продакшн. План отката. И всё это ради одного nullable string-поля.

С RedBase

Добавьте свойство в класс. Коммит. Деплой. Готово. Старые объекты работают. Новые объекты имеют поле. Пять минут.

Легаси-система с 500 хранимыми процедурами. Нельзя переписать, нельзя расширить.

Старая система работает. Еле-еле. Никто не хочет её трогать. Но новым фичам нужны новые структуры данных. Добавлять таблицы в легаси-схему — страшно.

С RedBase

Добавьте RedBase рядом. Новые фичи используют новый подход. Мигрируйте постепенно, когда будете готовы. Никакого «большого переписывания».

Товарам нужны разные поля. Одна таблица на тип? Или 1:1 JOIN-ы повсюду?

Одежде нужны Size и Color. Электронике нужны Memory и Processor. Традиционный подход: базовая таблица Products + ClothingDetails + ElectronicsDetails с 1:1 FK. Или одна гигантская таблица со 100 nullable-колонками. Оба варианта — кошмар поддержки.

Традиционный подход
-- Базовая таблица
CREATE TABLE Products (Id, Name, Price)

-- 1:1 таблицы расширения
CREATE TABLE ClothingDetails (ProductId FK, Size, Color)
CREATE TABLE ElectronicsDetails (ProductId FK, Memory, CPU)

-- Каждый запрос требует JOIN-ов
SELECT p.*, c.*, e.* FROM Products p
LEFT JOIN ClothingDetails c ON ...
LEFT JOIN ElectronicsDetails e ON ...
RedBase
// Просто используйте наследование C#
[RedbScheme] public class Product { ... }
[RedbScheme] public class Clothing : Product 
{ 
    public string Size { get; set; }
    public string Color { get; set; }
}
[RedbScheme] public class Electronics : Product 
{ 
    public int Memory { get; set; }
}

// Полиморфный запрос
var clothes = await redb.Query<Clothing>().ToListAsync();

Нужен List в сущности. Встречайте ещё одну таблицу.

У Order есть Items. У User есть Roles. У Product есть Tags. Каждая коллекция = новая таблица + внешний ключ + индекс + JOIN в каждом запросе. Даже для 3-5 элементов. Некоторые разработчики сдаются и хранят JSON-строки. Или значения через запятую. Полная потеря типизации и возможности запросов.

Традиционный подход
// Новая таблица: OrderItems
// Новая таблица: UserRoles  
// Новая таблица: ProductTags
// FK-ограничения, индексы, миграции...

var order = await context.Orders
    .Include(o => o.Items)
    .Include(o => o.Tags)
    .FirstAsync(o => o.Id == id);

// Или хак:
public string TagsJson { get; set; } // потеря типизации
public string RolesCsv { get; set; } // нельзя запрашивать
RedBase
[RedbScheme]
public class Order
{
    public List<OrderItem> Items { get; set; }
    public List<string> Tags { get; set; }
    public Dictionary<string, decimal> Prices { get; set; }
}

// Просто сохраните. Просто запросите. Готово.
await redb.SaveAsync(order);
var order = await redb.LoadAsync<Order>(id);

Нужны справочные таблицы. Города, Цвета, Материалы. У каждой FK, индексы, JOIN-ы.

Countries → таблица Cities с FK. Таблица Colors с FK. Таблица Materials с FK. А потом Cities нужны дополнительные данные — население, площадь, координаты. Расширить справочную таблицу? Но Colors этого не требуют. Добавить nullable FK на сущность City? А как насчёт Materials, которым тоже нужна ссылка? Ваши справочные таблицы превращаются в месиво из опциональных FK.

Традиционный подход
-- Справочные таблицы
CREATE TABLE Cities (Id, Name, CountryId FK)
CREATE TABLE Colors (Id, Name, Hex)
CREATE TABLE Materials (Id, Name)

-- Ой, Cities нужны дополнительные данные...
ALTER TABLE Cities ADD Population, Area, ...
-- Или сослаться на отдельную сущность?
ALTER TABLE Cities ADD CityEntityId FK NULL
-- Materials тоже? Ещё одна FK-колонка...

-- Фильтр по справочнику?
WHERE CityId IN (SELECT Id FROM Cities WHERE ...)
RedBase
// Встроенная поддержка ListItem
[RedbScheme]
public class Order
{
    public RedbListItem Status { get; set; }
    public RedbListItem City { get; set; }
    public List<RedbListItem> Tags { get; set; }
}

// Фильтр по значению
.Where(p => p.Status.Value == "Active")

// WhereIn
.WhereIn(p => p.Status.Value, ["Active", "Pending"])

// GroupBy тоже работает!
.GroupBy(p => p.City.Value)

Итог: выпускайте быстрее, переживайте меньше.

Разработка в 2 раза быстрее. Может, в 3 раза. Вы не воюете со схемами, миграциями, маппингами. Вы строите бизнес-логику. Доставляете фичи. Выпускаете продукты.

Дайте клиенту работающие бизнес-процессы — а не головную боль от базы данных.

Вся команда в выигрыше.

Бизнес-аналитик — говорит напрямую бизнес-сущностями. Не нужно переводить в схемы БД. Он описывает Order, Customer, Product — вы кодите Order, Customer, Product. Один язык.

QA-инженер — тестирует бизнес-логику, а не связи между таблицами. «Создать Order с 3 позициями», а не «INSERT в 5 таблиц в правильной последовательности».

Документация — больше никаких томов диаграмм БД. BA документирует бизнес-сущности. Вы добавляете технические заметки где нужно. Всё.

А что если спросят про базу данных?

Она прямо тут. Стандартный PostgreSQL или SQL Server. Полный доступ. Хотите кастомные хранимые процедуры? Пожалуйста. Нужен чистый SQL для сложного отчёта? Без проблем. Хотите расширить за пределы RedBase для чего-то экзотического? Ничто не мешает.

Начните просто. Масштабируйте сложно. Ваша бизнес-логика определяет архитектуру — а не наоборот.

2-3x Ускорение разработки
0 Миграций для отладки
100% Доступ к SQL когда нужно

Ваши данные имеют иерархию. Потом на разных уровнях появляются разные типы сущностей. И что теперь?

Представьте крупную корпорацию. Несколько штаб-квартир. Региональные офисы под каждой. Отделы в каждом офисе. Склады при отделах. Розничные точки. Шоу-румы. Товары на складах. И вот... товар переводят в штаб-квартиру для ревью.

Подождите — товар был на складе, но теперь он в... шоу-руме? В штаб-квартире? А завтра он может оказаться в розничной точке в другом городе?

Как вы смоделируете ЭТО в базе данных?

Традиционный подход

-- Кошмар «Корпоративной иерархии»
-- 
-- Сначала таблицы для каждого уровня:
CREATE TABLE Headquarters (id, name, ...);
CREATE TABLE RegionalOffices (id, hq_id, name, ...);
CREATE TABLE Departments (id, office_id, name, ...);
CREATE TABLE Warehouses (id, department_id, ...);
CREATE TABLE RetailPoints (id, department_id, ...);
CREATE TABLE Showrooms (id, office_id, ...);
CREATE TABLE Products (id, name, sku, ...);

-- А теперь самое интересное: ГДЕ находится товар?
-- Вариант A: Несколько FK-колонок
ALTER TABLE Products ADD warehouse_id INT NULL;
ALTER TABLE Products ADD retail_point_id INT NULL;
ALTER TABLE Products ADD showroom_id INT NULL;
ALTER TABLE Products ADD hq_showroom_id INT NULL;
-- Только ОДНА должна быть не NULL. Удачи с контролем.

-- Вариант B: Полиморфная ассоциация (подход «Rails»)
ALTER TABLE Products ADD location_type VARCHAR(50);
ALTER TABLE Products ADD location_id INT;
-- FK-ограничение невозможно. Осиротевшие записи гарантированы.

-- Вариант C: Связующие таблицы для каждой комбинации
CREATE TABLE Product_Warehouse (product_id, warehouse_id);
CREATE TABLE Product_RetailPoint (product_id, point_id);
CREATE TABLE Product_Showroom (product_id, showroom_id);
-- Теперь отслеживаем историю перемещений...
CREATE TABLE ProductMovementHistory (...);

-- Запрос: «Все товары под London HQ (любой уровень)»
-- 250 строк рекурсивных CTE по 7 таблицам
-- Производительность: 3-5 секунд на 100K товаров

-- Переместить товар в другое место?
-- BEGIN TRANSACTION;
-- DELETE FROM Product_Warehouse WHERE...
-- INSERT INTO Product_Showroom VALUES...
-- INSERT INTO ProductMovementHistory...
-- COMMIT;
-- Надеемся, что порядок правильный!

RedBase

// Определите сущности — просто C#-классы с атрибутом
[RedbScheme] public class Headquarters { ... }
[RedbScheme] public class Office { ... }
[RedbScheme] public class Department { ... }
[RedbScheme] public class Warehouse { ... }
[RedbScheme] public class Showroom { ... }
[RedbScheme] public class Product { ... }

// Создайте иерархию — любая сущность под любым родителем
await redb.CreateChildAsync(product, warehouse);
// Товар теперь на складе. Готово.

// Переместить товар в шоу-рум HQ? Одна строка:
await redb.MoveObjectAsync(product, hqShowroom);
// История отслеживается автоматически. Готово.

// Найти ВСЕ товары под London HQ (любая глубина)
var products = await redb
    .TreeQuery<Product>(londonHQ.Id, maxDepth: 10)
    .Where(p => p.InStock)
    .ToListAsync();
// Все товары со всех складов, шоу-румов,
// розничных точек под London. 15мс.

// Найти все отделы на уровне 3
var level3 = await redb
    .TreeQuery<Department>(hq.Id)
    .WhereLevel(3)
    .ToListAsync();

// Построить полное дерево со всеми дочерними элементами
var tree = await redb
    .LoadTreeAsync<Headquarters>(hq, maxDepth: 5);
// Навигация: tree.Children[0].Children[1].Props

Секрет: В RedBase иерархия встроена. У каждого объекта есть родитель. Разные типы сущностей могут располагаться на любом уровне. Товар сегодня может быть на складе, а завтра в шоу-руме — тот же API, та же таблица, ноль изменений схемы.

Но подождите, дальше ещё хуже...

MacBook стоит на полке склада. Перед отправкой в HQ на ревью директора кто-то кладёт в коробку набор аксессуаров. Кабель, чехол, может AirPods — для шефа.

Теперь коробка едет в HQ. В HQ могут достать аксессуар и оставить его там. Или весь комплект отправится в розницу — MacBook вместе с аксессуаром.

В традиционной БД: как смоделировать «аксессуар внутри коробки внутри склада»? Потом переместить коробку? Потом опционально отцепить аксессуар? Чую баги. Много багов.

Традиционный подход

-- Аксессуар внутри коробки? Ещё связующие таблицы!
CREATE TABLE ProductContents (
    container_id INT,
    content_id INT,
    added_date DATETIME,
    -- Стоп, а если содержимое имеет своё содержимое?
    -- Начинается рекурсивный кошмар...
);

-- Переместить коробку в HQ
UPDATE Products SET location_id = @hqId 
WHERE id = @boxId;
-- ОЙ! Аксессуар всё ещё ссылается на склад!
-- Нужен ручной фикс:
UPDATE Products SET location_id = @hqId 
WHERE id IN (SELECT content_id FROM ProductContents 
             WHERE container_id = @boxId);
-- А вложенное содержимое? Ещё рекурсия...

-- Отсоединить аксессуар в HQ?
DELETE FROM ProductContents 
WHERE container_id = @boxId AND content_id = @accId;
UPDATE Products SET location_id = @hqId 
WHERE id = @accId;
-- История? Аудит? Удачи.

RedBase

// Положить аксессуар внутрь коробки MacBook
await redb.CreateChildAsync(accessory, macbookBox);
// Готово. Аксессуар теперь ВНУТРИ коробки.

// Переместить всю коробку в HQ
await redb.MoveObjectAsync(macbookBox, hqShowroom);
// Аксессуар едет ВМЕСТЕ с коробкой. Автоматически.
// Никакого доп. кода. Никаких забытых обновлений.

// Достать аксессуар, оставить в HQ
await redb.MoveObjectAsync(accessory, hqShowroom);
// Аксессуар теперь напрямую под HQ.
// Коробка MacBook может ехать в розницу одна.

// Запрос: что внутри контейнера?
var contents = await redb
    .TreeQuery<Product>(macbookBox.Id)
    .ToListAsync();

// Полный аудит: где был аксессуар?
var path = await redb
    .GetPathToRootAsync<Product>(accessory);
// [Аксессуар] → [Коробка] → [Шоу-рум] → [HQ]

Суть: Связи «родитель-потомок» работают автоматически. Перемещаете родителя — потомки следуют за ним. Отцепляете потомка — он остаётся там, где вы его положили. Запросы на любом уровне. Никаких сирот. Никаких забытых обновлений. Никаких багов в 2 часа ночи.

Растите дальше. Вкладывайте глубже. Всё так же пара строк кода.

Завтра бизнес скажет: «Нужно отслеживать, какой сотрудник упаковал каждую коробку.» Добавьте свойство. Готово.

Через неделю: «Коробки могут содержать другие коробки.» Уже работает. Любой объект может быть родителем.

Через месяц: «Нам нужны транспортные контейнеры, содержащие несколько коробок с разных складов, едущие в разные пункты назначения, с прикреплёнными таможенными документами.» CreateChildAsync. MoveObjectAsync. TreeQuery. Тот же API. Ноль миграций.

Ваши конкуренты всё ещё пишут скрипты ALTER TABLE. А вы выпускаете фичи.

Но подождите... у объектов есть не только родители. Они ссылаются на ДРУГИЕ объекты.

Order ссылается на Customer. И на несколько Products. И на адрес доставки. И на историю платежей. И на купоны со скидками. И на менеджера по продажам, совершившего сделку.

А ещё у Customer свой Address. И список предыдущих Orders (циклическая ссылка!). А у Products есть Categories. И Suppliers. И Reviews от других Customers.

Теперь сохраните всё это. В правильном порядке. С правильными FK. Без сирот. Без дедлоков. Добро пожаловать в Ад Графов.

Традиционный подход

-- Order ссылается на Customer, Products, Payments...
CREATE TABLE Orders (
    id INT PRIMARY KEY,
    customer_id INT REFERENCES Customers(id),
    shipping_address_id INT REFERENCES Addresses(id),
    sales_manager_id INT REFERENCES Employees(id),
    discount_id INT NULL REFERENCES Discounts(id),
    -- Стоп, а как насчёт нескольких товаров?
);

-- Связующая таблица для Order-Product (many-to-many)
CREATE TABLE OrderProducts (
    order_id INT REFERENCES Orders(id),
    product_id INT REFERENCES Products(id),
    quantity INT,
    price_at_order DECIMAL,
    PRIMARY KEY (order_id, product_id)
);

-- Ещё одна связующая для истории платежей
CREATE TABLE OrderPayments (
    order_id INT REFERENCES Orders(id),
    payment_id INT REFERENCES Payments(id),
    -- ...
);

-- Сохранить заказ? Вот кошмар:
BEGIN TRANSACTION;
-- 1. Убедиться что Customer существует (или создать)
-- 2. Убедиться что Address существует (или создать)  
-- 3. INSERT Order (сначала нужен customer_id!)
-- 4. INSERT OrderProducts для каждого товара
-- 5. INSERT Payments
-- 6. INSERT OrderPayments-связи
-- 7. UPDATE Order с итогами оплаты
-- Надеемся, что ничего не упало между шагами!
COMMIT;

-- Удалить заказ? Ужас CASCADE ждёт.
-- Загрузить заказ со всеми связями? 15 JOIN-ов.

RedBase

// Просто опишите бизнес-модель
[RedbScheme]
public class OrderProps
{
    public Customer Customer { get; set; }
    public Address ShippingAddress { get; set; }
    public Employee SalesManager { get; set; }
    
    // Массив объектов? Просто объявите.
    public Product[] Products { get; set; }
    
    // Массив RedbObjects со своими Props!
    public RedbObject<PaymentProps>[] Payments { get; set; }
    
    // Словарь объектов? Почему нет!
    public Dictionary<string, RedbObject<CouponProps>> 
        Coupons { get; set; }
}

// Сохранить весь граф объектов
var order = new RedbObject<OrderProps>
{
    Props = new OrderProps
    {
        Customer = customer,
        Products = new[] { laptop, mouse, keyboard },
        Payments = new[] { payment1, payment2 },
        Coupons = new Dictionary<string, ...>
        {
            ["SUMMER20"] = summerDiscount
        }
    }
};

await redb.SaveAsync(order);
// ВСЕ вложенные объекты сохранены. Все связи созданы.
// Одна строка. Одна транзакция. Готово.

// Загрузить с полным графом
var loaded = await redb.LoadAsync<OrderProps>(id);
// loaded.Props.Customer — готово
// loaded.Props.Products[0] — готово
// loaded.Props.Payments[1].Props — готово
// loaded.Props.Coupons["SUMMER20"] — готово

Графы объектов — полноценные граждане первого класса. Объявляйте ссылки, массивы, словари объектов в Props. Сохраните один раз — весь граф персистируется. Загрузите один раз — весь граф материализуется. Никаких связующих таблиц. Никакого кошмара FK. Никаких сирот. Никогда.

Customer Customer
Ссылка на один объект
Product[] Products
Массив бизнес-классов
RedbObject<T>[] Items
Массив полных объектов с ID
Dictionary<K, T>
Коллекции чего угодно по ключу

«Удалить... но не совсем. И показать прогресс.»

Мягкое удаление. Добавьте `is_deleted` в каждую таблицу. Не забудьте фильтровать его в КАЖДОМ запросе. Забыли один раз — пользователи видят удалённые данные. Или хуже — удалённые данные портят отчёты.

Нужно реально удалить? Ручные скрипты. Пакетное удаление. «Сколько осталось?» — никто не знает, пока не закончится. Или не упадёт.

Традиционный подход

-- Добавить is_deleted ВЕЗДЕ
ALTER TABLE Orders ADD is_deleted BIT DEFAULT 0;
ALTER TABLE Customers ADD is_deleted BIT DEFAULT 0;
ALTER TABLE Products ADD is_deleted BIT DEFAULT 0;
-- ... ещё 50 таблиц

-- КАЖДЫЙ запрос требует это:
SELECT * FROM Orders 
WHERE is_deleted = 0  -- Забудете = баг
  AND customer_id IN (
    SELECT id FROM Customers WHERE is_deleted = 0
  );

-- Очистить? Удачи:
DECLARE @batch INT = 1000;
WHILE 1=1 BEGIN
    DELETE TOP(@batch) FROM Orders 
    WHERE is_deleted = 1;
    IF @ROWCOUNT = 0 BREAK;
    -- Прогресс? Какой прогресс?
END

RedBase

// Вариант 1: Синхронное удаление с прогрессом в реальном времени
var progress = new Progress<PurgeProgress>(p =>
{
    var pct = p.Total > 0 ? p.Deleted * 100 / p.Total : 0;
    Console.WriteLine($"{p.Deleted}/{p.Total} ({pct}%)");
});

await redb.DeleteWithPurgeAsync(ids, batchSize: 100, progress);
// Прогресс в реальном времени. Пакетно. Безопасно.

// Вариант 2: Фоновый сервис «запустил и забыл»
var mark = await deletionService.DeleteAsync(ids, user);
// Возвращается СРАЗУ. Очистка идёт в фоне.

// Проверить прогресс в любой момент:
var status = await deletionService.GetProgressAsync(mark.TrashId);
// status.Deleted, status.Remaining, status.Status

// Query() автоматически фильтрует удалённые
var orders = await redb.Query<Order>().ToListAsync();
// Только активные. Никакого is_deleted. Никогда.

// Кластер-безопасно: переживает рестарты!

Запустил и забыл. Объекты перемещаются в контейнер-корзину, фоновый сервис очищает пакетами. Прогресс хранится в БД — переживает рестарты. Кластер-безопасно. Никакого засорения `is_deleted`. Query() фильтрует автоматически.

«Найти заказы, где Customer.Address.City = 'London'»

Поиск по вложенным свойствам. В традиционной ORM: 5 JOIN-ов. 50 строк SQL. Забыли Include? Ошибка в рантайме. Переименовали свойство? Ищите все 47 мест.

Традиционный подход

SELECT o.* 
FROM Orders o
JOIN Customers c ON o.customer_id = c.id
JOIN Addresses a ON c.address_id = a.id
JOIN Cities ct ON a.city_id = ct.id
WHERE ct.name = 'London'
  AND o.is_deleted = 0
  AND c.is_deleted = 0
  AND a.is_deleted = 0;
-- Забыли JOIN? Неправильные результаты.
-- Переименовали City в Location? Ищите все запросы.

RedBase

var orders = await redb.Query<Order>()
    .Where(o => o.Customer.Address.City == "London")
    .ToListAsync();
// Одна строка. Типобезопасно. Рефакторинг-friendly.
// Переименовали City? Компилятор найдёт ВСЕ использования.

«Опять N+1. Третий раз за спринт.»

Забыли Include? Цикл загружает 1000 клиентов по одному. Производительность падает. Добавили Include — теперь грузите слишком много данных. Баланс между eager и lazy loading — вечная битва.

Традиционная ORM

var orders = await context.Orders
    .Include(o => o.Customer)      // забыли = N+1
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)  // и это тоже
    .Where(o => o.Status == "Active")
    .ToListAsync();
// Пропустили один Include = баг производительности
// Добавили слишком много = раздувание памяти

RedBase

var orders = await redb.Query<Order>()
    .Where(o => o.Status == "Active")
    .ToListAsync();
// Полный граф объектов. Всегда. Один запрос.
// Никаких цепочек Include. Никаких N+1. По дизайну.

«Кто это изменил? Когда? Какое было значение до?»

Аудит. Создайте таблицы аудита. Напишите триггеры. 500 строк шаблонного кода. «Покажи историю этого заказа» — кастомный код для каждой сущности.

Традиционный подход

-- Таблица аудита для КАЖДОЙ сущности
CREATE TABLE Orders_Audit (
    id INT, order_id INT,
    changed_by INT, changed_at DATETIME,
    old_value NVARCHAR(MAX),
    new_value NVARCHAR(MAX)
);

-- Триггер для КАЖДОЙ таблицы
CREATE TRIGGER tr_Orders_Audit ON Orders
AFTER UPDATE AS BEGIN
    INSERT INTO Orders_Audit (...)
    SELECT ... FROM inserted JOIN deleted ...
END;
-- Повторить x50 таблиц. Поддерживать вечно.

RedBase

// Встроено в КАЖДЫЙ объект:
order.DateCreate   // Когда создан
order.DateModify   // Последнее изменение
order.WhoChangeId  // Кто изменил
order.OwnerId      // Владелец объекта

// Запрос по изменениям:
var recentChanges = await redb.Query<Order>()
    .WhereRedb(o => o.DateModify > yesterday)
    .ToListAsync();

// Никаких триггеров. Никаких таблиц аудита.
// Метаданные — часть каждого RedbObject.

«Это МОЙ объект. Только я должен его видеть.»

Владение объектами. Создайте таблицу прав доступа. FK повсюду. Сложные JOIN-ы в каждом запросе. Забыли один раз — утечка данных.

Традиционный подход

-- Таблица прав доступа
CREATE TABLE ObjectPermissions (
    object_id INT, object_type VARCHAR(50),
    user_id INT, permission VARCHAR(20)
);

-- КАЖДЫЙ запрос требует это:
SELECT o.* FROM Orders o
JOIN ObjectPermissions p 
  ON p.object_id = o.id 
  AND p.object_type = 'Order'
WHERE p.user_id = @currentUser
  AND p.permission = 'read';
-- Полиморфный FK. Нет ссылочной целостности.
-- Забыли JOIN = дыра в безопасности.

RedBase

// owner_id встроен в каждый RedbObject
var order = new RedbObject<OrderProps>
{
    OwnerId = currentUser.Id,
    Props = new OrderProps { ... }
};

// Фильтр по владельцу:
var myOrders = await redb.Query<Order>()
    .WhereRedb(o => o.OwnerId == currentUser.Id)
    .ToListAsync();

// Встроено. Типобезопасно. Без лишних таблиц.

«Обновить 100 000 объектов. Без таймаута. С прогрессом.»

Массовые операции. Ручное пакетирование. Обработка таймаутов. Отслеживание прогресса вручную. Скачки памяти. Исчерпание пула соединений. Сюрпризы в пятницу в 17:00.

Традиционный подход

// Кошмар ручного пакетирования
var allItems = await GetAllItems(); // Память!
var batches = allItems.Chunk(1000);
var processed = 0;

foreach (var batch in batches)
{
    using var tx = BeginTransaction();
    foreach (var item in batch)
    {
        item.Status = "Processed";
        await context.SaveChangesAsync(); // Медленно!
    }
    tx.Commit();
    processed += batch.Length;
    // Прогресс? Console.WriteLine, наверное...
}
// Таймаут? Начинаем заново.

RedBase Pro

// Массовое сохранение с параллельной обработкой
var items = await redb.Query<Item>().ToListAsync();

// Модификация параллельно
Parallel.ForEach(items, item => 
{
    item.Props.Status = "Processed";
});

// Массовое сохранение — оптимизированные пакеты
await redb.SaveAsync(items);
// Внутри: BulkInsert, BulkUpdate, транзакции.

// Удаление с прогрессом в реальном времени:
var progress = new Progress<PurgeProgress>(p =>
    Console.WriteLine($"{p.Deleted}/{p.Total}"));

await redb.DeleteWithPurgeAsync(ids, 
    batchSize: 100, progress);
// Фоновая очистка. Кластер-безопасно.

Общая картина: вы создаёте не просто приложение

С иерархиями, полиморфными деревьями, графами объектов и полным LINQ — вы не пишете очередное бизнес-решение. Вы строите платформу.

Мини-CRM. BPM-движок. No-code конструктор. IoT-хаб данных. Какой бы ни была ваша область — слой базы данных решён. Вы фокусируетесь на бизнес-логике. Выпускаете фичи. Впечатляете заказчиков.

Ваша экспертиза раскрывается в полной мере — не погребённая под ORM-обвязкой. Решения остаются элегантными, поддерживаемыми, профессиональными. Ваш продукт превосходит ожидания — потому что вы больше не воюете с базой данных.

Добавьте AI — и у вас платформа нового поколения. Структурированные данные + строгая типизация + LINQ-запросы = идеальный фундамент для интеграции AI/ML.

Ноль экзотических рисков

PostgreSQL / SQL Server — проверенные, корпоративного класса
Никаких экзотических графовых БД с мануалом на 10 страниц
Никакого нетипизированного JSONB, где баги прячутся до продакшна
Никаких внешних NoSQL-кластеров для деплоя и поддержки
Cloud-ready — Azure, AWS, GCP, on-prem — ваш выбор

Традиционная база данных. Революционная продуктивность. Это обещание RedBase.

Корпоративная иерархия — разные типы на каждом уровне
Штаб-квартира Headquarters
Офис Лондон Office
Офис Берлин Office
Склад А Warehouse
Шоу-рум Showroom
Розничная точка RetailPoint
iPhone 15 Product
MacBook Pro Product Перемещение в HQ
// Перемещение товара между ЛЮБЫМИ локациями
await redb.MoveObjectAsync(macbook, hqShowroom);
TreeQuery
Запрос любого поддерева с полной поддержкой LINQ
MoveObjectAsync
Мгновенное перемещение объектов между родителями
Полиморфная иерархия
Разные типы сущностей на любом уровне
WhereLevel / WhereLeaves
Запрос по глубине, поиск листовых узлов
GetPathToRoot
Мгновенное построение хлебных крошек
Recursive CTE
Оптимизированный SQL под капотом
WhereChildrenOf
Прямые дочерние элементы любого узла
WhereRoots
Мгновенный поиск всех корневых узлов
WhereHasAncestor<T>
Поиск по условию типа родителя
WhereHasDescendant<T>
Поиск по условию типа потомка
ToRootListAsync
Загрузка полной структуры дерева
GetDescendantsAsync
Все потомки на любой глубине
SaveAsync(graph)
Сохранение всего графа объектов за раз
ChangeTracking
Автоматическое обнаружение изменений графа
LoadAsync<T>
Полная материализация графа

Сравнение RedBase

Сравнение возможностей с популярными альтернативами

Функция RedBase EF Core Dapper MongoDB JSONB PostgreSQL
Строгая типизация ~ ~
Полная поддержка LINQ ~
Ноль миграций
Вложенные объекты ~
Древовидные запросы (CTE)
Полиморфная иерархия ~ ~
Графы объектов ~ ~
Запросы к вложенным Props ~
ACID-транзакции ~
Прямой доступ к SQL
Массовые операции ~ ~ ~
Встроенные поля аудита
Владелец / Права доступа
ListItem (справочники)
Оконные функции ~
Без схемы на сущность ~
Внешние ключи
Агрегации (Sum, Avg...)
GroupBy
Change Tracking
Мягкое удаление + фоновая очистка
Встроенные пользователи, роли и авторизация
Превью SQL (ToSqlString) ~
Вычисляемые выражения ~ ~ ~
Индексы ~
Хранимые процедуры
Поддержка нескольких СУБД
Dictionary<Tuple, T>
Экспорт/Импорт (.redb) ~
Полная поддержка ~ Частично / Вручную Не поддерживается
EF Core — отличная ORM, но требует миграций, ручных цепочек Include, нет встроенных древовидных запросов
Dapper — быстрая микро-ORM, но только чистый SQL, нет маппинга объектов или вложенных структур
MongoDB — гибкие документы, но нет ACID по умолчанию, другой язык запросов, нет мощи SQL
JSONB — JSON в SQL, но теряете типизацию, сложные запросы, ограничения индексов
RedBase: Multi-DB — PostgreSQL или SQL Server, один и тот же код. Экспорт в .redb файл, импорт куда угодно. Никакой привязки к вендору.
RedBase: Деревья — иерархии встроены. TreeQuery, MoveObjectAsync, GetPathToRoot — никаких CTE вручную.
RedBase: Мягкое удаление — атомарная пометка + фоновая очистка как встроенный BackgroundService. Прогресс хранится в БД (переживает рестарты). Кластер-безопасно: брошенные задачи восстанавливаются с атомарным захватом при старте. Query() автоматически фильтрует удалённые — никакого is_deleted нигде.
RedBase: Пользователи и авторизация — полное управление пользователями из коробки. CreateUser, ValidateUser (SHA256+salt), Roles, Permissions (CRUD по объекту/схеме), SecurityContext, CreateSystemContext. Без зависимости от ASP.NET Identity — работает в консольных приложениях, сервисах, Blazor.
RedBase: Превью SQL — вызовите ToSqlString() на любом запросе, чтобы увидеть точный SQL перед выполнением. Без настройки логирования, без перехватчиков. Скопируйте в pgAdmin, запустите EXPLAIN ANALYZE. Полная прозрачность.
Мы используем свой продукт Этот сайт документации хранит весь контент — страницы, примеры, API-справочники — как RedB-объекты в базе MSSQL. Без CMS, без статических файлов. Просто RedBase.

Попробуйте. 15 минут до первого запроса.

Без проектирования схемы. Без миграций. Определите классы, сохраните объекты, запрашивайте через LINQ.

2-3x ускорение разработки
0 миграций
1 024 бесплатных Pro-запросов на старте

Open Source ядро. Pro-функции с пробным лимитом. Начните бесплатно, масштабируйте когда нужно.