7 мифов о Linq to Database

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 запрос.Вот краткий набор мифов, которые можно встретить. Если у вас есть еще — дополняйте.

© Habrahabr.ru