...

вторник, 22 июля 2014 г.

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-провайдеров и как выполняются запросы в СУБД. Подробнее в предыдущем посте — http://ift.tt/1zZVAeA


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


Например в 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 запрос.

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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий