Помогаю зарабатывать (или не тратить) с помощью ИТ
–0,1
рейтинг
22 июля 2014 в 10:15

Разработка → 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 Tracking.

Миф №7


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

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

Вот краткий набор мифов, которые можно встретить. Если у вас есть еще — дополняйте.
Стас Выщепан @gandjustas
карма
20,5
рейтинг –0,1
Помогаю зарабатывать (или не тратить) с помощью ИТ
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (21)

  • +5
    Для некоторых платформ, это совсем не мифы :)

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


    Могу сказать, что в Windows Phone это чистая правда. Так как, там не реализован Emit, то маппинг происходит исключительно за счет рефлексии.

    Так же там база не поддерживает кеширование и все запросы всегда выполняются на базе.
    • 0
      Извините за чайниковский вопрос в двух вариантах — допустим, у нас есть маленькая домашняя бухгалтерия:
      вариант первый — телефон подключен к сети, есть клиентское приложение — кто мешает облегчить все и вынести все тяжелое на сервер?

      вариант второй — телефон не подключен в сети — командировка, отпуск, другая страна, интернет отсутствует/дорогой — работа с кешированными данными только оффлайн, синхронизация с какой-то периодичностью (бесплатная сеть в отеле вечером)
      ну и в день, я допустим совершаю несколько раз перекусить, проезды, покупка сувениров, музеи всякие…

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

        Но вопроса до конца так и не понял, насчет разрядки, зависит не от объема данных, а от времени работы CPU. Некоторые даже используют DirectX для отображения графиков, что тоже сказывается на заряде батареи.
        • 0
          я про Вашу ругань на скорость маппинга в WP
          • 0
            Ну это не ругань, это факт.
            Просто попробуйте создать чуть более сложную схема БД со многими связями один-ко-многим, которые также нужны при выборке.
            Для себя мы нашли выход вместо маппинга в Entity использовать проекцию в анонимные классы.
  • +2
    >Но во всех провайдерах такой анализ проводится один раз, а потом данные кешируются.
    Для L2SQL нужно использовать CompiledQuery — т.е. сам провайдер не кеширует — чтобы анализ происходил только 1 раз.
    Для EF провайдер кеширует только на время открытия контекста, т.е. разбор linq дерева будет повторятся множество раз (если не воспользоваться тем же фокусом как и в l2sql).
    Возможно, я вас неправильно понял или неправ)
    • 0
      До .net 4.5 так и было. Потом анонсировали улучшения в l2s, но я не проверял какое сейчас поведение.
  • +1
    Иногда генератор SQL-я работает совершенно неисповедимо: stackoverflow.com/questions/5205281/very-different-performance-of-ef-with-very-similar-queries
    • 0
      Прежде чем писать такой код, почитайте предыдущий пост — habrahabr.ru/post/230479/
      • +1
        Тот код был написан 3 года назад. Не очень понимаю какой совет из предыдущего поста мне бы там помог? Для меня непонятно как генератор из такого элементарного выражения нагородил тот огород который там приведен в вопросе:

        var scope = model.Scopes.Include("Settings") .Where(s => (s.Level == intLevel && s.Name == name)) .First();
        • 0
          Все дело в Include
          1) Бага в EF, описана в этом посте в мифе #2
          2) Надо делать проекции, ибо при джоине двух таблиц без проекций генерируется довольно большой Dataset.
  • 0
    В SQL Server есть механизм Plan Guide, который позволяет навесить хинты на любой запрос.

    Он позволит навесить WITH(NOLOCK) на одну из таблиц, используемых в запросе?
    • +1
      да, но лучше изучите другие варианты.
      • 0
        А что будет если обновится EF, и начнет немного по-другому строить запросы. Все мои plan guides придется переписать?
        • 0
          Если будет много plan guides, то проблема вовсе не в ef.
          Еще раз повторю слова поста — Linq позволяет описать только очень простые запросы, которые прекрасно оптимизируются SQL Server и Oracle. Если СУБД не оптимизирует запрос, то значит не хватает индексов\статистики\ограничений. Поэтому хинты вряд ли нужны будут. NOLOCK тоже не нужно ставить, лучше запросы оптимизировать, чтобы интервал блокировок был меньше. На крайний случай установить уровень изоляции read uncommitted. А в идеале поставить RCS.
  • 0
    А как решается задача удаления объектов из таблицы/таблиц по каким-нибудь критериям?
    Требуется ли для этого сначала эту коллекцию прочитать?
    • 0
      Зависит от провайдера, EF и Linq2DB позволяют выполнять DML. L2S и NHibernate, насколько мне известно — нет.
  • +1
    По поводу первого мифа:
    Думал, что такой подход неоптимален и неудобен, пока на двух последних проектах не использовал эту практику. Выделенный человек писал и оптимизировал запросы и структуру базы. Разработчики приложения также писали SQL код, но всё уходило на ревью. В итоге с производительностью на базе никогда проблем не было, код качественный, чистый и аккуратный.

    Из минусов:
    — много одинакового кода (но из-за особенностей sql server'а не всё можно вынести в общие процедуры/функции/вью)
    — при большом количестве разработчиков датабазник становится бутылочным горлышком

    Для сравнения заглянул в репозиторий проекта, где все писали SQL код и иногда использовали Linq To Sql. Казалось бы каждый нормальный бэкенд разработчик должен на уровне понимать и писать SQL, но как оказалось, реальность далека от идеала.
  • 0
    То есть мифы №№5 и 6 — не мифы.
  • 0
    Про Миф №2 есть возражения:
    Where(x=>x.IsActive == false)
    Будет ввиде
    Where not (IsActive = 1)

    И такой запрос не обрабатывается индексом.
    • 0
      Почему не обрабатывается?

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