7 мифов о Linq to Database

в 6:15, , рубрики: .net, entity framework, linq, sql, разработка

Linq появился в 2007 году, тоже же появился первый IQueryable-провайдер — Linq2SQL, он работал только с MS SQL Server, довольно сильно тормозил и покрывал далеко не все сценарии. Прошло почти 7 лет, появилось несколько Linq-провайдеров, которые работают с разными СУБД, победили почти все «детские болезни» технологии и, уже пару лет как, Linq to Database (обобщенное название для популярных провайдеров) готов к промышленному применению.

Тем не менее далеко не все применяют Linq to Database и объясняют это не только тем, что проект старый и переписать на linq довольно сложно, но и приводят в качестве аргументов различные мифы. Эти мифы кочуют из одной компании в другую и часто распространяются через интернет.

В этом посте я собрал самые популярные мифы и опровержения к ним.

Миф №1

Базой данных занимается специально обученный DBA, который делает все запросы, а программисты пишут код, поэтому Linq to Database не нужен.

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

Если DBA не обладает таким знанием, то обычно сводится к тому, что DBA делает небольшой набор CRUD хранимок на каждую сущность + несколько хранимок для самых «толстых» запросов. А остальное уже делается программистами в коде. Это чаще всего неэффективно работает, потому что в среднем тянется сильно больше данных, чем нужно для конкретного сценария. И оптимизировать такое сложно.

Если же DBA знает каждый сценарий, то у него два варианта:
а) Сделать много хранимок (почти одинаковых), каждую под конкретный сценарий, а потом мучительно их поддерживать.
б) Сделать несколько универсальных хранимок с кучей параметров, внутри которых клеить строки для формирования оптимальных запросов. Причем добавление дополнительного параметра в запрос становится крайне сложным процессом.

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

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

Миф №2

Linq генерирует неэффективные SQL запросы.

Очень часто повторяемый миф. Но большая часть неэффективности Linq запросов создается людьми.

Причины этому простые:
1) Люди не понимают чем отличается Linq от SQL. Linq работает с упорядоченными последовательностями, а SQL с неупорядоченными множествами. Поэтому некоторые Linq операции добавляют в SQL крайне неэффективные операторы сортировки.
2) Люди не понимают механизмов работы IQuryable-провайдеров и как выполняются запросы в СУБД. Подробнее в предыдущем посте — habrahabr.ru/post/230479

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

Например в Entity Framework есть баг при использовании навигационных свойств:

context.Orders
       .Where(o => o.Id == id)
       .SelectMany(o => o.OrderLines)
       .Select(l => l.Product)
       .ToList();

Такой запрос генерирует следующий SQL:

Много кода

    [Project1].[Id] AS [Id], 
    [Project1].[OrderDate] AS [OrderDate], 
    [Project1].[UserId] AS [UserId], 
    [Project1].[C1] AS [C1], 
    [Project1].[OrderId] AS [OrderId], 
    [Project1].[ProductId] AS [ProductId], 
    [Project1].[Id1] AS [Id1], 
    [Project1].[Title] AS [Title]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[OrderDate] AS [OrderDate], 
        [Extent1].[UserId] AS [UserId], 
        [Join1].[OrderId] AS [OrderId], 
        [Join1].[ProductId] AS [ProductId], 
        [Join1].[Id] AS [Id1], 
        [Join1].[Title] AS [Title], 
        CASE WHEN ([Join1].[OrderId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM  [dbo].[Orders] AS [Extent1]
        LEFT OUTER JOIN  (SELECT [Extent2].[OrderId] AS [OrderId], [Extent2].[ProductId] AS [ProductId], [Extent3].[Id] AS [Id], [Extent3].[Title] AS [Title]
            FROM  [dbo].[OrderLines] AS [Extent2]
            INNER JOIN [dbo].[Products] AS [Extent3] ON [Extent2].[ProductId] = [Extent3].[Id] ) AS [Join1] ON [Extent1].[Id] = [Join1].[OrderId]
        WHERE [Extent1].[Id] = @p__linq__0
    )  AS [Project1]
    ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC

В этом запросе вычисляемое поле и сортировка по нему не могут быть соптимизированы SQL Server и приходится выполнять реальную сортировку.

Но если немного переписать Linq запрос на использование оператора join, то проблемы не будет:

var orders1 = from o in context.Orders
                where o.Id == id
                join ol in context.OrderLines on o.Id equals ol.OrderId into j
                from p in  j.DefaultIfEmpty()
                select p.Product;

orders1.ToArray();

Полученный SQL:

SELECT 
    [Extent3].[Id] AS [Id], 
    [Extent3].[Title] AS [Title]
    FROM   [dbo].[Orders] AS [Extent1]
    LEFT OUTER JOIN [dbo].[OrderLines] AS [Extent2] ON [Extent1].[Id] = [Extent2].[OrderId]
    LEFT OUTER JOIN [dbo].[Products] AS [Extent3] ON [Extent2].[ProductId] = [Extent3].[Id]
    WHERE [Extent1].[Id] = @p__linq__0

Он отлично покрывается индексами и оптимизируется SQL Server.

Также слышал о неэффективных запросах NHibernate, но не работал с ним настолько активно, чтобы найти такие баги.

Миф №3

Медленно работает маппинг.

Само преобразование DataReader в набор объектов выполняется за доли микросекунды на каждый объект. Причем linq2db провайдер умудряется делать это быстрее, чем разрекламированный Dapper.

А вот что может работать медленно, так это присоединение полученных объектов к Change Tracking контексту. Но это необходимо выполнять только в случае, когда объекты будут изменены и записаны в базу. В остальных случаях можно явно указать чтобы объекты не присоединялись к контексту или использовать проекции.

Миф №4

Медленно генерируются запросы.

Действительно для генерации SQL запроса из Linq требует обхода дерева, много работы с рефлексией и анализ метаданных. Но во всех провайдерах такой анализ проводится один раз, а потом данные кешируются.

В итоге для простых запросов генерация запроса выполняется в среднем за 0,4мс. Для сложных это может быть до нескольких миллисекунд.
Это время обычно меньше статистической погрешности от общего времени выполнения запроса.

Миф №5

Нельзя использовать хинты.

В SQL Server есть механизм Plan Guide, который позволяет навесить хинты на любой запрос. Аналогичные механизмы есть и в других СУБД.

Но даже при этом хинты не сильно нужны при использовании Linq. Linq генерирует довольно простые запросы, которые СУБД самостоятельно оптимизирует при наличии статистики, индексов и ограничений. Хинты блокировок лучше заменить на выставление правильных уровней изоляции и ограничение количества запрашиваемых строк.

Миф №6

В Linq нельзя использовать все возможности SQL.

Отчасти это правда. Но многие возможности SQL можно завернуть в функции или представления, а их уже использовать в Linq запросах.

Более того, Entity Framework позволяет выполнять любые SQL запросы, а результаты мапить на объекты, в том числе с Change Traking.

Миф №7

Хранимые процедуры работают быстрее ad-hoc запросов, генерируемых Linq.

Это было актуально в середине 90-х годов. Сегодня все СУБД «компилируют» запросы и кешируют планы, независимо от того процедура это или ad-hoc запрос.

Вот краткий набор мифов, которые можно встретить. Если у вас есть еще — дополняйте.

Автор: gandjustas

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js