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