.NET Tutorial
Middle6 мин чтенияВопрос 11 из 14

Raw SQL, FromSql, ExecuteSql — когда уходить от LINQ?

Инструменты

да

нет

Что нужно?

Сущности + дальше LINQ

FromSql / FromSqlInterpolated

DTO или скаляры

Database.SqlQuery<T>

Массовое изменение

Выражается LINQ?

ExecuteUpdate / ExecuteDelete

Database.ExecuteSql

Сложная read-модель,
максимальная скорость

Dapper на соединении EF

да

нет

Что нужно?

Сущности + дальше LINQ

FromSql / FromSqlInterpolated

DTO или скаляры

Database.SqlQuery<T>

Массовое изменение

Выражается LINQ?

ExecuteUpdate / ExecuteDelete

Database.ExecuteSql

Сложная read-модель,
максимальная скорость

Dapper на соединении EF

Какой API выбрать для SQL

FromSql

C#
string term = request.Search;                       // пользовательский ввод

var products = await db.Products
    .FromSql($"""
        SELECT * FROM products
        WHERE to_tsvector('russian', name || ' ' || description) @@ plainto_tsquery('russian', {term})
        """)
    .Where(p => p.IsActive)                          // LINQ поверх SQL
    .OrderBy(p => p.Price)
    .Take(20)
    .AsNoTracking()
    .ToListAsync(ct);

EF превращает это в:

SQL
SELECT p.* FROM (
    SELECT * FROM products
    WHERE to_tsvector('russian', name || ' ' || description) @@ plainto_tsquery('russian', @p0)
) AS p
WHERE p.is_active
ORDER BY p.price
LIMIT @p1

Требования FromSql: SQL должен возвращать все столбцы сущности с правильными именами; для последующей композиции LINQ — быть композируемым (не EXEC процедуры в SQL Server).

Параметризация и SQL-инъекции

C#
// БЕЗОПАСНО: интерполированная строка FormattableString → {term} станет параметром @p0
db.Products.FromSql($"SELECT * FROM products WHERE name = {term}");

// БЕЗОПАСНО: Raw с явными параметрами
db.Products.FromSqlRaw("SELECT * FROM products WHERE name = {0}", term);
db.Products.FromSqlRaw("SELECT * FROM products WHERE name = @name", new NpgsqlParameter("name", term));

// ОПАСНО: строка собрана заранее — интерполяция уже выполнена, EF получает готовый текст
var sql = $"SELECT * FROM products WHERE name = '{term}'";
db.Products.FromSqlRaw(sql);   // term = "x' OR 1=1; DROP TABLE products; --"
База данныхEF CoreКодFormattableString:формат иаргументыраздельнозначение параметра — всегда данные,никогда не код SQLFromSql($"... WHERE name= {term}")SQL: ... WHERE name =@p0, параметр @p0 = "x' OR1=1 --"
База данныхEF CoreКодFormattableString:формат иаргументыраздельнозначение параметра — всегда данные,никогда не код SQLFromSql($"... WHERE name= {term}")SQL: ... WHERE name =@p0, параметр @p0 = "x' OR1=1 --"
Почему FromSql безопасен

SqlQuery — DTO и скаляры

C#
public sealed record SalesByDay(DateOnly Day, decimal Revenue, int Orders);

var stats = await db.Database.SqlQuery<SalesByDay>($"""
    SELECT date_trunc('day', created_at)::date AS "Day",
           SUM(total) AS "Revenue",
           COUNT(*)   AS "Orders"
    FROM orders
    WHERE created_at >= {from}
    GROUP BY 1
    ORDER BY 1
    """).ToListAsync(ct);

int count = await db.Database.SqlQuery<int>($"SELECT COUNT(*) AS \"Value\" FROM orders").SingleAsync(ct);

ExecuteSql и массовые операции

C#
// Произвольная команда
int affected = await db.Database.ExecuteSqlAsync($"CALL recalc_customer_stats({customerId})", ct);

// EF Core 7+: массовые операции LINQ — без загрузки сущностей в память
await db.Sessions.Where(s => s.ExpiresAt < DateTime.UtcNow).ExecuteDeleteAsync(ct);

await db.Products
    .Where(p => p.CategoryId == categoryId)
    .ExecuteUpdateAsync(s => s
        .SetProperty(p => p.Price, p => p.Price * 1.1m)
        .SetProperty(p => p.UpdatedAt, DateTime.UtcNow), ct);

Dapper рядом с EF Core

C#
public sealed class OrderReadQueries(ShopDbContext db)
{
    public async Task<IReadOnlyList<OrderSummary>> TopCustomersAsync(DateTime from, CancellationToken ct)
    {
        var conn = db.Database.GetDbConnection();              // то же соединение и транзакция EF
        const string sql = """
            SELECT c.id, c.name, SUM(o.total) AS total,
                   RANK() OVER (ORDER BY SUM(o.total) DESC) AS rank
            FROM customers c JOIN orders o ON o.customer_id = c.id
            WHERE o.created_at >= @from
            GROUP BY c.id, c.name
            LIMIT 50
            """;
        var rows = await conn.QueryAsync<OrderSummary>(new CommandDefinition(sql, new { from }, cancellationToken: ct));
        return rows.AsList();
    }
}

Типичная схема: EF Core для команд (изменение агрегатов, миграции, Change Tracker), Dapper или raw SQL для сложных запросов чтения (отчёты, дашборды).

Когда уходить от LINQ

Ситуация Почему SQL лучше
Оконные функции, рекурсивные CTE LINQ не выражает их (или выражает неэффективно)
Специфичные функции СУБД Полнотекстовый поиск, jsonb-операторы, геоданные, LATERAL, DISTINCT ON
Подсказки оптимизатору, блокировки FOR UPDATE SKIP LOCKED, WITH (NOLOCK), OPTION (RECOMPILE)
Сгенерированный SQL неэффективен Лишние подзапросы, неудачные JOIN — проверено в плане выполнения
Хранимые процедуры, legacy Логика уже в БД
Аналитика по большим объёмам Вычисления на стороне БД, минимум данных в приложение

Прежде чем писать SQL — посмотрите, что генерирует EF (ToQueryString()), и попробуйте переписать LINQ (проекции, AsSplitQuery, EF.Functions). Raw SQL не проверяется компилятором и ломается при переименовании столбцов.

Вопросы на засыпку

Чем FromSql отличается от FromSqlRaw?

FromSql / FromSqlInterpolated принимают FormattableString и превращают каждое {значение} в параметр. FromSqlRaw принимает обычную строку — параметры нужно передавать явно ({0} или DbParameter). Ошибка — собрать строку интерполяцией и передать в Raw.

Как посмотреть SQL, который сгенерирует LINQ-запрос?

query.ToQueryString() — возвращает SQL с объявлениями параметров без выполнения. Плюс логирование (LogTo) и инструменты вроде MiniProfiler.

Отслеживаются ли сущности из FromSql?

Да, как и в обычном запросе (если нет AsNoTracking): их можно изменить и сохранить через SaveChanges. SqlQuery<T> для не-сущностей не отслеживается.

Как вызвать хранимую процедуру с OUTPUT-параметром?

Через ExecuteSqlRawAsync с SqlParameter { Direction = ParameterDirection.Output } и чтением .Value после выполнения. Или через ADO.NET/Dapper на соединении db.Database.GetDbConnection().

Итог

EF Core даёт безопасные способы выполнить SQL: FromSql для сущностей с дальнейшей композицией, SqlQuery<T> для DTO и скаляров, ExecuteSql для команд, ExecuteUpdate/Delete для массовых операций. Используйте интерполяцию для параметров, белые списки для идентификаторов и уходите в SQL, когда LINQ не выражает задачу или генерирует неэффективный запрос.