Методы оптимизации LINQ-запросов в C#.NET

  • Tutorial

Введение


В этой статье рассматривались некоторые методы оптимизации LINQ-запросов.
Здесь же приведем еще некоторые подходы по оптимизации кода, связанные с LINQ-запросами.

Известно, что LINQ(Language-Integrated Query) — это простой и удобный язык запросов к источнику данных.

А LINQ to SQL является технологией доступа к данным в СУБД. Это мощный инструмент работы с данными, где через декларативный язык конструируются запросы, которые затем будут преобразовываться в SQL-запросы платформой и отправляться на сервер баз данных уже на выполнение. В нашем случае под СУБД будем понимать MS SQL Server.

Однако, LINQ-запросы не преобразовываются в оптимально написанные SQL-запросы, которые смог бы написать опытный DBA со всеми нюансами оптимизации SQL-запросов:

  1. оптимальные соединения (JOIN) и фильтрование результатов (WHERE)
  2. множество нюансов в использовании соединений и групповых условий
  3. множество вариаций в замене условий IN на EXISTSи NOT IN, <> на EXISTS
  4. промежуточное кэширование результатов через временные таблицы, CTE, табличные переменные
  5. использование предложения (OPTION) с указаниями и табличных хинтов WITH (...)
  6. использование индексируемых представлений, как одно из средств избавиться от избыточных чтений данных при выборках

Основными узкими местами производительности получающихся SQL-запросов при компиляции LINQ-запросов являются:

  1. консолидация всего механизма отбора данных в одном запросе
  2. дублирование идентичных блоков кода, что в итоге приводит к многократным лишним чтениям данных
  3. группы многосоставных условий (логических «и» и «или») — AND и OR, соединяясь в сложные условия, приводит к тому, что оптимизатор, имея подходящие некластеризованные индексы, по необходимым полям, в конечном итоге все же начинает делать сканирование по кластерному индексу (INDEX SCAN) по группам условий
  4. глубокая вложенность подзапросов делает очень проблематичным разбор SQL-инструкций и разбор плана запросов со стороны разработчиков и DBA

Методы оптимизации


Теперь перейдем непосредственно к методам оптимизации.

1) Дополнительное индексирование


Лучше всего рассматривать фильтры на основных таблицах выборки, поскольку очень часто весь запрос строится вокруг одной-двух основных таблиц (заявки-люди-операции) и со стандартным набором условий (IsClosed, Canceled, Enabled, Status). Важно для выявленных выборок создать соответствующие индексы.

Данное решение имеет смысл, когда выбор по этим полям существенно ограничивает возвращаемое множество запросом.

Например, у нас есть 500000 заявок. Однако, активных заявок всего 2000 записей. Тогда правильно подобранный индекс избавит нас от INDEX SCAN по большой таблице и позволит быстро выбрать данные через некластеризованный индекс.

Также нехватку индексов можно выявить через подсказки разбора планов запросов или сбора статиcтик системных представлений MS SQL Server:

  1. sys.dm_db_missing_index_groups
  2. sys.dm_db_missing_index_group_stats
  3. sys.dm_db_missing_index_details

Все данные представления содержат сведения об отсутствующих индексах, за исключением пространственных индексов.

Однако, индексы и кэширование часто являются методами борьбы последствий плохо написанных LINQ-запросов и SQL-запросов.

Как показывает суровая практика жизни для бизнеса часто важна реализация бизнес-фичей к определенным срокам. И потому часто тяжелые запросы переводят в фоновый режим с кэшированием.

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

Данный подход позволяет решать запросы бизнеса, но понижает в итоге работоспособность информационной системы, просто отсрочивая решения проблем.

Также стоит помнить о том, что в процессе поиска необходимых для добавления новых индексов, предложения MS SQL по оптимизации могут быть некорректными в том числе при следующих условиях:

  1. если уже существуют индексы с подобным набором полей
  2. если поля в таблице не могут быть проиндексированы по причине ограничений индексирования (более детально об этом описано здесь).

2) Объединение атрибутов в один новый атрибут


Иногда некоторые поля из одной таблицы, по которым происходит группа условий, можно заменить введением одного нового поля.

Особенно это актуально для полей-состояний, которые по типу обычно являются либо битовыми, либо целочисленными.

Пример:

IsClosed = 0 AND Canceled = 0 AND Enabled = 0 заменяется на Status = 1.

Здесь вводится целочисленный атрибут Status, обеспечиваемый заполнением этих статусов в таблице. Далее проводится индексирование этого нового атрибута.

Это фундаментальное решение проблемы производительности, ведь Мы обращаемся за данными без лишних вычислений.

3) Материализация представления


К сожалению, в LINQ-запросах нельзя напрямую использовать временные таблицы, CTE и табличные переменные.

Однако, есть еще один способ оптимизации на этот случай — это индексируемые представления.

Группа условий (из примера выше) IsClosed = 0 AND Canceled = 0 AND Enabled = 0 (или набор других схожих условий) становится хорошим вариантом для использования их в индексируемом представлении, кэшируя небольшой срез данных от большого множества.

Но есть ряд ограничений при материализации представления:

  1. использование подзапросов, предложений EXISTS должны заменяться использованием JOIN
  2. нельзя использовать предложения UNION, UNION ALL, EXCEPTION, INTERSECT
  3. нельзя использовать табличные хинты и предложения OPTION
  4. нет возможности работы с циклами
  5. невозможно выводить данные в одном представлении из разных таблиц

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

Но при вызове представления эти индексы могут не использоваться, а для явного их использования необходимо указывать WITH (NOEXPAND).

Поскольку в LINQ-запросах нельзя определять табличные хинты, так что приходится делать еще одно представление — «обертку» следующего вида:

CREATE VIEW ИМЯ_представления AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);

4) Использование табличных функций


Часто в LINQ-запросах большие блоки подзапросов или блоки, использующие представления со сложной структурой, образуют конечный запрос с очень сложной и не оптимальной структурой выполнения.

Основные преимущества использования табличных функций в LINQ-запросах:

  1. Возможность, как и в случае с представлениями, использовать и указывать как объект, но можно передать набор входных параметров:
    FROM FUNCTION(@param1, @param2 ...)
    в итоге можно добиться гибкой выборки данных
  2. В случае использования табличной функции нет таких сильных ограничений, как в случае с индексируемыми представлениями, описанных выше:

    1. Табличные хинты:
      через LINQ нельзя указывать какие индексы необходимо использовать и определять уровень изоляции данных при запросе.
      Но в функции эти возможности есть.
      С функцией можно добиться достаточно постоянного плана запроса выполнения, где определенны правила работы с индексами и уровни изоляции данных
    2. Использование функции позволяет, по сравнению с индексируемыми представлениями, получить:

      • сложную логику выборки данных (вплоть до использования циклов)
      • выборки данных из множества разных таблиц
      • использование UNION и EXISTS

  3. Предложение OPTION очень полезно, когда нам надо обеспечить управление параллелизмом OPTION(MAXDOP N), порядком плана выполнения запроса. Например:

    • можно указать принудительное пересоздание плана запроса OPTION (RECOMPILE)
    • можно указать необходимость обеспечить принудительное использование планом запроса порядка соединения, указанного в запросе OPTION (FORCE ORDER)

    Более детально про OPTION описано здесь.
  4. Использование самого узкого и требуемого среза данных:
    Нет необходимости держать большие наборы данных в кэшах (как в случае с индексируемыми представлениями), из которых еще необходимо по параметру до фильтровать данные.
    Например, есть таблица, у которой для фильтра WHERE используются три поля (a, b, c).

    Условно для всех запросов есть постоянное условие a = 0 and b = 0.

    Однако, запрос к полю c более вариативный.

    Пусть условие a = 0 and b = 0 нам действительно помогает ограничить требуемый получаемый набор до тысяч записей, но условие по с нам сужает выборку до сотни записей.

    Здесь табличная функция может оказаться более выигрышным вариантом.

    Также табличная функция более предсказуема и постоянна по времени выполнения.

Примеры


Рассмотрим пример реализации на примере базы данных Questions.

Есть запрос SELECT, соединяющий в себе несколько таблиц и использующий одно представление (OperativeQuestions), в котором проверяется по email принадлежность (через EXISTS) к «Активным запросам»([OperativeQuestions]):

Запрос № 1
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM [dbo].[Questions] AS [Extent1]
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id],
[Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId],
[Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]) AND ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[OperativeQuestions] AS [Extent5]
WHERE (([Extent5].[Email] = @p__linq__0) OR (([Extent5].[Email] IS NULL) 
AND (@p__linq__0 IS NULL))) AND ([Extent5].[Id] = [Extent1].[Id])
));


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

Выборка из OperativeQuestions порядка десяти тысяч записей.

Основная проблема этого запроса в том, что для записей из внешнего запроса выполняется внутренний подзапрос на представлении [OperativeQuestions], который должен для [Email] = @p__linq__0 нам ограничить выводимую выборку (через EXISTS) до сотен записей.

И может показаться, что подзапрос должен один раз рассчитать записи по [Email] = @p__linq__0, а потом эти пару сотен записей должны соединяться по Id c Questions, и запрос будет быстрым.

На самом же деле происходит последовательное соединение всех таблиц: и проверка соответствия Id Questions с Id из OperativeQuestions, и фильтрование по Email.

По сути запрос работает со всеми десятками тысяч записей OperativeQuestions, а ведь нужны только интересующие данные по Email.

Текст представления OperativeQuestions:

Запрос № 2
 
CREATE VIEW [dbo].[OperativeQuestions]
AS
SELECT DISTINCT Q.Id, USR.email AS Email
FROM            [dbo].Questions AS Q INNER JOIN
                         [dbo].ProcessUserAccesses AS BPU ON BPU.ProcessId = CQ.Process_Id 
OUTER APPLY
                     (SELECT   1 AS HasNoObjects
                      WHERE   NOT EXISTS
                                    (SELECT   1
                                     FROM     [dbo].ObjectUserAccesses AS BOU
                                     WHERE   BOU.ProcessUserAccessId = BPU.[Id] AND BOU.[To] IS NULL)
) AS BO INNER JOIN
                         [dbo].Users AS USR ON USR.Id = BPU.UserId
WHERE        CQ.[Exp] = 0 AND CQ.AnswerId IS NULL AND BPU.[To] IS NULL 
AND (BO.HasNoObjects = 1 OR
              EXISTS (SELECT   1
                           FROM   [dbo].ObjectUserAccesses AS BOU INNER JOIN
                                      [dbo].ObjectQuestions AS QBO 
                                                  ON QBO.[Object_Id] =BOU.ObjectId
                               WHERE  BOU.ProcessUserAccessId = BPU.Id 
                               AND BOU.[To] IS NULL AND QBO.Question_Id = CQ.Id));


Исходный маппинг представления в DbContext (EF Core 2)
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}


Исходный LINQ-запрос
var businessObjectsData = await context
    .OperativeQuestions
    .Where(x => x.Email == Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


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

Хотя это и хорошее решение, есть и другой вариант оптимизации данной задачи.

Основная цель — закэшировать записи по [Email] = @p__linq__0 из представления OperativeQuestions.

Вводим табличную функцию [dbo].[OperativeQuestionsUserMail] в базу данных.

Отправляя как входной параметр Email, получаем обратно таблицу значений:

Запрос № 3

CREATE FUNCTION [dbo].[OperativeQuestionsUserMail]
(
    @Email  nvarchar(4000)
)
RETURNS
@tbl TABLE
(
    [Id]           uniqueidentifier,
    [Email]      nvarchar(4000)
)
AS
BEGIN
        INSERT INTO @tbl ([Id], [Email])
        SELECT Id, @Email
        FROM [OperativeQuestions]  AS [x] WHERE [x].[Email] = @Email;
     
    RETURN;
END


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

Чтобы запросы к OperativeQuestionsUserMail были оптимальны, имели оптимальные планы запросов, необходима строгая структура, а не RETURNS TABLE AS RETURN

В данном случае искомый Запрос 1 преобразуется в Запрос 4:

Запрос № 4
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM (
    SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] (@p__linq__0)
) AS [Extent0]
INNER JOIN [dbo].[Questions] AS [Extent1] ON([Extent0].Id=[Extent1].Id)
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id], [Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId], [Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]);


Маппинг представления и функции в DbContext (EF Core 2)
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}
 
public static class FromSqlQueries
{
    public static IQueryable<OperativeQuestion> GetByUserEmail(this DbQuery<OperativeQuestion> source, string Email)
        => source.FromSql($"SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] ({Email})");
}


Итоговый LINQ-запрос
var businessObjectsData = await context
    .OperativeQuestions
    .GetByUserEmail(Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


Порядок времени выполнения понизился с 200-800 мс, до 2-20 мс., и т. д., т е в десятки раз быстрее.

Если более усреднено брать, то вместо 350 мс получили 8 мс.

Из очевидных плюсов также получаем:

  1. общее снижение нагрузки по чтением,
  2. значительное уменьшение вероятности блокировок
  3. уменьшение среднего времени блокировки до приемлемых значений

Вывод


Оптимизация и тонкая настройка обращений к БД MS SQL через LINQ является задачей, которую можно решить.

В данной работе очень важны внимательность и последовательность.

В начале процесса:

  1. необходимо проверить данные, с которыми работает запрос (значения, выбранные типы данных)
  2. провести правильное индексирование этих данных
  3. проверить корректность соединительных условий между таблицами

На следующей итерации оптимизации выявляются:

  1. основа запроса и определяется основной фильтр запроса
  2. повторяющиеся схожие блоки запроса и анализируется пересечение условий
  3. в SSMS или другом GUI для SQL Server оптимизируется сам SQL-запрос (выделение промежуточного хранилища данных, построение результирующего запроса с использованием этого хранилища (может быть несколько))
  4. на последнем этапе, беря за основу результирующий SQL-запрос, перестраивается структура LINQ-запроса

В итоге получившийся LINQ-запрос должен стать по структуре идентичным выявленному оптимальному SQL-запросу из пункта 3.

Благодарности


Огромное спасибо коллегам jobgemws и alex_ozr из компании Fortis за помощь в подготовке данного материала.

Комментарии 24

    +7
    Спасибо за полезную информацию! Правда из своего опыта работы с БД из .NET, используя ORM пришел к выводу, что проще и эффективнее либо писать чистый SQL с маппингом результатов (Dapper, EF .SqlQuery), либо организовывать хранилище данных таким образом, чтобы для «боевых» выборок никогда не требовалось бы что-то сложнее простого select + where и несложных group by.
      +1
      Расскажите, как обновляете БД? Пишете всю раскладку со сравнениями изменённых данных для всего графа?
        +3
        Немного не понял вопрос. Если вы о об изменении данных в таблицах, то зачастую все эти операции можно доверить ORM. Простые insert, update by id транслируются в адекватный SQL. Сложных запросов, даже типа 'update where', где условие включает в себя нечто большее чем match по индексируемому полю я так же стараюсь избегать. Вообще сейчас, когда есть возможность использовать объемные и надежные СХД все больше update становятся insert (event sourcing), а о delete лучше вообще забыть.
        Если про схему, то механизм миграций в EF — отличная вещь. Если чего-то не хватает в штатном генераторе — всегда можно дописать SQL в миграции. Если хочется отказаться от EF, то нужен отдельный скрипт под VCS, позволяющий создать/обновить базу. Хорошо, если в таком случае ведением таких скриптов будет заниматься отдельный сотрудник, который в случае чего сможет и написать скрипт под сложную миграцию базы с данными.
          +1
          Да, я про изменение данных.

          Ясно, по первому комментарию решил, что вы отказались от полноценной ORM и перешли на чистый SQL, а вы используете сразу и то, и то.
      +2

      спасибо! всегда хотелось выработать какую-то стратегию совмещения EF с оптимизацией

        +2
        спасибо за статью, очень интересно было прочитать.
        хотелось бы поинтересоваться у сообщества, кто какой подход предпочитатет в плане того, какое количество логики писать на c#/linq и какое на стороне базы через хранимые процедуры/функции?

        я по своему опыту предпочитаю держать слой c# максимально тонким и работать с данными через представления для чтения и хранимые процедуры для обновления. Тогда можно менять структуру базы без проблем, главное чтобы представления и процедуры не ломали контракт.
        А тяжелые отчеты которые занимают время однозначно на чистом SQL через процедуры.

        Хотелось бы узнать у кого какой опыт?
          +2

          Примерно такой же стратегии придерживаюсь по возможности

        • НЛО прилетело и опубликовало эту надпись здесь
            +1

            Делать многосоставной кластерный индекс, в котором может быть много полей — это спорный вариант для производительности, частые операции модификации могут сделать их обслуживание дорогим, а выборки начнут простаивать в блокировках ( все же таблицы достаточно большие). Фильтрованный индекс — это хорошее решение, но делать его для достаточно сложных предикатов отбора — это опять же закладывать камень под производительность. Все таки пересечения условий могут быть очень размазаны по всему возможному диапазону значений, да и содержать сложные сочетания условий and, or, is not null.

            • НЛО прилетело и опубликовало эту надпись здесь
              +1

              Достаточно часто встречал случаи, когда фильтруемый индекс не то, что не работал, а даже в подсказках не мог быть использован.
              По ому с ним надо очень аккуратно-уж лучше секционированный индекс или по старинке разбить таблицы на оперативные и неоперативные данные.

              –3
              Сколько лет уже существует linq 2 SQL / EF — столько я убеждаюсь, что правильно сделал, что не поддался хайпу, и оставил классический принцип построения систем:
              1. RDBMS, спроектированная профессионалом
              2. объекты RDBMS, написанные автором п.1
              3. Максимально возможная бизнес-логика внутри п.1,2
              4. Взаимодействие с БД только через п.2
              5. Обертки над п.4 на процедурном ЯП и бизнес-логика, которую сложно/невозможно сделать в п.1,2
              6...99999 Любые обертки и надстройки сверху п.5

              Если бизнес-требования к системе более-менее устойчивы, то гораздо проще и надежнее сделать п.1,2 на T-SQL, чем пытаться извращениями добиться хотя бы порядково похожей эффективности на Linq2SQL/Linq2Entities, особенно в системах со сложной моделью данных и правилами их модификации.
              Пункт 3 позволяет передавать бизнес-знания всего ОДНОМУ специалисту, и этот специалист как раз является инженером высокой квалификации.
              Далее, п.4 гарантирует, что ни один джун не напишет кривой запрос, который выгрузит на клиент пол-базы, забрав в качестве payload всего один скаляр, и вообще, НЕ БУДЕТ никаких взаимодействий с БД, ни на чтение, ни на запись помимо определенных в п.2 точек входа. Мы просто запрещаем пермиссиями обращаться к данным иначе, чем через объявленные объекты (процедуры, TVF, вьюхи)
              Следствием детерминирования доступа к БД является полезная возможность комбинировать средства синхронизации доступа к данным со средствами потоковой синхронизации — (например sp_getapplock). Для длинных бизнес-действий часто надежнее выстроить их в один поток этими средствами, чем использование serializable-транзакций

              В п.6 и выше можно использовать специалистов любого уровня — они не смогут при всем желании ни испортить бизнес-целостность данных, ни существенно повлиять на производительность.

              Где же здесь место для EF/Linq2Entities? Оно очень удобно для генерации оберточных классов вокруг сигнатур SP и резалтсетов возврата. Не более того.

              Отладка такой системы очень проста — п.1,2 проверяются тест-скриптами, которые вызывают объекты БД, имитируя карту действий фронтенда. Для п.6 и выше можно использовать тестовые БД с предопределенными данными, а можно и данные, полученные прогоном тестовых скриптов из предыдущего пункта
              Performance optimization делается тоже очень прозрачно — все вызовы детерминированы, а T-SQL код в них написан человеком и легко читаем/понимаем. Оптимизировать объекты БД можно прямо на лету, при условии, что сигнатуры и резалтсеты методов БД не меняются.
              Имена вызываемых методов видны сразу в SQL Profiler, и обычно сразу видно что, для чего, и в каком порядке вызывается — сравните с адовой лапшой, которая льется SQL-текстом из Linq2SQL в профайлер без малейшей возможности без скрупулезного разбора понять, что именно в нем делается.

              Да, тривиальный бакенд можно очень быстро накидать на code-first, но подумайте, останется ли он простым? Если концепт пойдет, не придется ли переделывать все заново? И потом, простые концепты обычно показывают бизнес-пиплу — а им нужно, чтобы было красиво. Так вот, для простых концептов дельта затрат на то, чтобы сделать T-SQL средствами модель, и обернуть ее методы EF по сравнению с code first, ничтожна относительно затрат на разработку дизайна, верстку и построение фронтенда
              • НЛО прилетело и опубликовало эту надпись здесь
                  +1
                  С динамикой конечно сложнее, и прежде всего не потому, что нужно помнить и бояться sql-инъекций. Существует класс задач с динамическим построением резалтсетов по метаданным, которые вообще плохо решаемы статическими обертками, но, например, хорошо решаемы с использованием старых добрых нетипизированных датасетов. Код бакенда разрабатывается одним (чаще всего его хватает, если разработка линейна, даже для больших систем) профессионалом, и это достаточная гарантия правильной работы с пользовательским вводом.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Подумайте тогда для сравнения, во что превратилась бы система, если по модели в несколько сотен таблиц каждый разработчик ездил запросами вдоль и поперек, в массовых модификациях данных вместо множественных операций использовались бы единичные вставки и обновления в циклах в десятки тысяч итераций, а lazy load глубиной в 4 этажа вешался бы намертво из-за невозможности указания префиксов?

                      Linq2Entities — это отрубить себе пальцы рук, мотивируя тем, что «теперь никто не прорисовывает детали, а грубо можно и культей», затем понять, что для тонких/нестандарнтых вещей пальцы все-таки нужны — но их уже нет, и поэтому придумать 10 воркэраундов, как с гораздо меньшим успехом теперь рисовать ногами.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          +1
                          Есть фундаментальное противоречие, неразрешимое в общем виде никакими средствами: RDBMS — это системы, работающие в терминах множеств. Чем ловчее вы для них определяете множества — тем эффективнее они работают на вас. Процедурные ЯП — это системы, работающие в терминах нитей исполнения. Чем ловчее вы декомпозируете алгоритм на параллельные нити — тем эффективнее они работают на вас.
                          Руками и в частном случае можно добиться какой-то адекватной проекции второго метода на первый с точки зрения эффективности. В общем случае — нет. Поэтому для решения задач работы с данными нужно использовать именно средства работы с данными, а не обертки.

                          Это если еще не говорить про эффективность обработки данных «на месте». Если про нее говорить, да еще в контексте атомарности — все проблемы умножаются на порядок. Вынуть полбазы под транзакцией, модифицировать и записать назад итератором по одному — как вам такое?

                          И наконец, доказательство:

                          1. разработка программного функционала стоит денег
                          2. программный функционал реализует свойства системы, за счет которых она эффективно (конкурентноспособно) продается
                          3. только малая часть функционала RDBMS от Microsoft поддерживается в обертке их же производства — большая часть может быть использована только не-ORM средствами
                          4. от версии к версии количество такого не-ORM функционала, по крайней мере, не уменьшается (на самом деле — растет)
                          => существует более эффективный способ работы с RDBMS, использующий весь имеющийся функционал, и поскольку объем такого функционала растет — значит, компания вкладывается в его поддержку => понимает, что без него не достичь таких показателей работы, чтобы оставаться конкурентноспособным на SMB-рынке. А поскольку, этот способ — T-SQL, это значит, что коммерчески для построения бакендов для автоматизации SMB T-SQL остается более подходящим.
                    0
                    Думал, обойдусь парой предложений…

                    А динамичные запросы вы через строки и sp_executesql делаете? В конце концов где-то что-то пропустите и вас нагнут через sql injection.
                    Нет, конечно, так нет необходимости делать. Динамические запросы нужно строить так, чтоб никакого пользовательского ввода в код запроса просто не попадало. Это всегда можно, кроме редчайших случаев типа построения констант для некоторых вариантов полнотекстового поиска на MS SQL — вот в этих и только этих случаях да, приходится писать обертки, которые не позволят выбраться зловредному пользовательскому вводу за пределы строковых констант. Даже для запросов с 100500 вариантами фильтров, соединений и выражений в том же MS SQL, если не хотите использовать условия запуска подветок, обычно очень неплохо подходит запрос с перекомпиляцией, если вы хорошо понимаете, как написать сколь угодно большой запрос так, чтобы оптимизатор для начала гарантированно отрубил все неиспользуемые ветви, а затем эффективно построил план для оставшейся части — и да, для этого знать про option(recompile) совершенно недостаточно.

                    Чистый SQL не очень подходит для бизнес логики, кроме совсем простой
                    Это попросту не так. И да, я видел и некоторое время поучаствовал в разработке такой огромной системы. Там, совершенно как в примере gleb_l, точно такое отношение к безопасности — базовые права гарантирует сам сервер, остальное — процедурной частью, а логика вообще вся на сервере, ни одного запроса мимо интерфейсных объектов, да мимо и невозможно — см. пункт про права на объекты БД. Или же ваше утверждение требует переформулировки или дополнений, например — по факту очень трудно найти специалистов БД, которые способны все это спроектировать, расписать и при необходимости развивать и поддерживать. Например, в сравнением с рынком специалистов по C# — это очень заметно, факт.

                    код бакенда разрабатывается одним профессионалом
                    У меня как раз система с горами такого. Спасибо, за двадцать лет там столько «профессионалов» поучавствовало, что волосы встают дыбом, там где их и быть-то не должно.
                    Видите, у вас ровно обратный хрестоматийный пример — вместо разработки грамотным профессионалам — «столько «профессионалов» поучавствовало» (цитата), с предсказуемыми последствиями. Подход же, озвученный gleb_l, при условии выполнения грамотным специалистом (я понимаю, что специалистом по SQL считает себя чуть менее, чем каждый каждый первый, кто умеет написать простой запрос, но сейчас я именно про грамотных профильных специалистов) очень устойчив при использовании разношерстной командой, уже в одном этом его огромный плюс.

                    Чистый SQL не очень подходит для бизнес логики, кроме совсем простой. Можно конечно через SQL CLR делать, но тоже так себе удовольствие. На Оракле конечно повеселее, а для SQL server-а это занятие для не слабых духом
                    Это говорит лишь о том, что с Ораклом вы сколько успели проработать, а с MSSQL — нет. Тут все дело в том, что подходы к ним очень, очень разные. При переходе от одного к другому — неважно, в каком порядке — сначала необходимо полностью перестроить мозги, сломать привычное мышление, совершенно изменить подход. Начинать писать на t/sql и обнаружить, что в нем отсутствуют привычные так необходимые объекты и пайпланы или, наоборот, перейти на Оракл и обескураженно искать, где же тут локальные времянки, без которых жизнь как без воздуха, и непосредственная отдача резалтсета из процедурной части клиенту — поначалу просто апатия, проклятия и ненависть, без преувеличения. Холивары MS SQL vs Oracle потому так и унылы и однообразны, что читаешь аргументы что одной, что другой строны — и видишь только, что аргументаторы привыкли мыслить только одной парадигмой, а противоположная обеим сторонам кажется какой-то инопланетной дикостью, поэтому эффективно мыслить за обе стороны мало кто пытается. В действительности же и у той, и другой стороны есть свои сильные и слабые стороны, опять же хрестоматийный случай.
                    Ну и да, никто обходиться без CLR не заставляет, даже совершенно не для написания на нем бизнес-логики, а исключительно для базовых вещей, которые почему-то до сих пор не завезли в MS SQL, например, почему-то отсутствующего инструментария для использования регулярок, из-за чего у каждого разработчика есть своя библиотечка на этом CLR для оберток над функционалом для работы с ними. Видите, я использовал аргумент из первой десятки в пользу Оракла, где регулярки давно из коробки :)
                    +6
                    Бизнес-логике не место в бд так как SQL для неё не предназначен
                      +2
                      И все же все не все так безнадежно.
                    +3
                    В названии статьи отсутствует LINQ by EF. Так как оно не отражает полную картину. EF всегда был калекой в трансляции дерева выражений в SQL.

                    Практически все эти детские болезни мы полечили в linq2db. Клеить строки не надо, Dapper не надо, делать хранимки на каждый чих не надо.

                    • Хинты можно использовать
                    • CTE из каробки, вместе с рекурсивными CTE
                    • Union, Concat собирает все что надо, хоть завались ими
                    • INSERT FROM, INSERT INTO, UPDATE FROM...
                    • Менять названия таблиц налету
                    • Использовать временные таблицы
                    • Оконные функции из каробки
                    • BulkCopy на все поддерживаемые базы данных
                    • CRUD по CQRS паттерну, ничего лишнего вплоть до единственного поля
                    • ...


                    “Но они продолжали жевать кактус и дискутировать какой generic repository лучше и когда стартовать UoW.”
                      +2
                      Годная вещь. Я правильно понимаю, что можно подцепить его к EF Core с сохранением миграций, и пользоваться всей мощью linq2db?
                        +4
                        Да, есть библиотека для бескровного склеивания двух миров. Асинки вот только конфликтуют, для решения этой ситуации добавлены дополнительные экстеншины.
                        github.com/linq2db/linq2db.EntityFrameworkCore

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое