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

Разработка → 3 самые плохие вещи, которые можно сделать с помощью Linq to Database

SQL*, .NET*
Дисклеймер: Под Linq to Database здесь и далее понимаем самые распространенные .NET библиотеки для доступа к реляционным СУБД с использованием Linq запросов. Такие как Linq2SQL, Entity Framework, NHibername, Linq2db и подобные
С момента появления IQueryable провайдеров для доступа к базам данных писать запросы стало гораздо легче. С одной стороны это хорошо, потому что приложения стало делать гораздо легче. С другой стороны качество запросов сильно упало. И дело не в том, что провайдеры генерируют плохой SQL, а в том, что люди пишут запросы, не понимая как оно работает под капотом. Для многих C# программистов IQueryable является магией, и появляется естественное желание уменьшить влияние магии на приложения, изолировать магию от остального кода. Такой подход не только не исправляет ситуацию, а усугубляет её…

Пример кода

В одном из недавних холиваров один из участников опубликовал потрясающий пример кода:
public IEnumerable<DocumentDto> GetDocumentsToday()
{
    foreach (var item in Db.Documents.Where(x => EntityFunctions.TruncateTime(x.CreatedOn) == DateTime.Today))
    {
        yield return Mapper.Map<Document, DocumentDto>(item);
    }
}

Предполагалось что этот код сможет изолировать «сложность» IQueryable провайдеров от потребителя. Но именно этот, с виду простой, код содержит 3 плохие вещи при работе с Linq to Database.

Итак хит-парад:

Третье место

Долгая обработка результатов запроса. Большинство провайдеров удерживают открытым DataReader пока идет перечисление IQueryable. Это приводит к тому, что все ресурсы, выделенные сервером для этого запроса, удерживаются до окончания обхода и закрытия DataReader. В данном примере еще используется yield return, то есть обработка каждого элемента может идти очень долго.

В случае использования SQL Server (или другой блокировочной БД) при уровне изоляции Repeatable Read блокировки будут также держаться до конца обработки результатов запроса, а если будет эскалация, то и на всю таблицу.

Как исправить
Всегда явно выполнять материализацию результатов, например с помощью .Load() или .ToList() или .ToArray(). Если же результаты запроса очень большие, а надо втянуть все, то лучше всего организовать пейджинг блоками, которые не вызовут эскалацию блокировок.

Второе место

Неэффективный предикат. В данном примере предикат преобразуется в SQL в нечто вроде такого в зависимости от провайдера:
...
WHERE cast(CreatedOn as Date) = cast(GETDATE() as Date)
Естественно ни одни индекс не сможет оптимизировать такую выборку и будет полный обход таблицы.

Даже если в этом конкретном запросе такой предикат не будет проблемой, то код имеет тенденцию быть скопированным в другие места вместе со всеми проблемами.

Как исправить
Не использовать функции и преобразования типов для значений колонок в предикатах и джоинах. Этот пример можно переписать так:
Db.Documents.Where(x => x.CreatedOn >= DateTime.Today && x.CreatedOn < DateTime.Today.AddDays(1))
Теперь можно будет сделать индекс по полю CreatedOn.

Первое место

Отсутствие проекции. Казалось бы банальная вещь — запрос тянет все поля таблицы, а потом мапит на DTO, который может иметь гораздо меньше полей… Если в таблице есть LOB поля, то это может дать очень негативно повлиять на скорость выполнения запроса. В некоторых ORM можно настроить отложенную загрузку LOB полей, но даже в этом случае наличие проекции эффективнее. При наличии проекции в запросе можно построить покрывающий индекс, то есть индекс, включающий в себя все колонки в проекции. То есть проекции позволяют не только сделать запрос быстрее, но и дать возможность дальнейшей оптимизации на уровне базы данных.

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

Исправленный пример

public IEnumerable<DocumentDto> GetDocumentsToday()
{
    return Db.Documents
             .Where(x => x.CreatedOn >= DateTime.Today && x.CreatedOn < DateTime.Today.AddDays(1))
             .Select (x => new DocumentDto {...})
             .ToList();
}
Можно также поправить класс Mapper, чтобы он генерировал проекции.

Заключение

В этом хит-параде собраны три самые частые проблемы, которые возникают при использовании Linq. Я оптимизировал много приложений и почти всегда находил эти проблемы.

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

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

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

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

  • +1
    .Select (x => new DocumentDto {...})

    Скажите пожалуйста, а Entity Framework разрешает уже подобную запись конструктора внутри запроса до AsEnumerable()/ToList()? Или только анонимные типы пока?
    • +2
      С первых версий разрешал. В Linq2SQL было ограничение, что нельзя делать проекцию на тип, на который замаплена таблица, иначе это бы ломало Change Tracking. В EF возможно есть такое же ограничение, надо проверить, хотя маловероятно.
  • +4
    Продолжать обязательно!
  • +1
    Хранимые процедуры уже совсем вышли из моды?

    По мне, так получить ещё один управляемый метод абстракции, будет полезен не только разработчику, но и DBA.
    • +1
      Совсем. Linq позволяет сформировать запрос очень точно под каждый сценарий, учеть все проекции, фильтры, постраничную разбивку. Если использовать процедуры, то надо или делать кучу процедур под каждый сценарий (а потом мучительно их поддерживать), или клеить строки внутри процедур в SQL, что еще хуже, чем клеить строки руками в приложении.
      Процедуры могут помочь когда возможностей Linq-провайдеров недостаточно, но заменить Linq, не потеряв темп разработки и\или быстродействие, почти нереально.

      • 0
        Детальное сравнение LINQ-to-SQL и хранимых процедур: stackoverflow.com/a/28992
        • 0
          Красиво, конечно, написано… Но в реале не всё так раждужно как хотелось-бы получить.

          Но вот только даже сам стек написан с использованием как прямого SQL'я, так и LINQ2SQL:
          meta.stackexchange.com/questions/72499/how-do-you-use-linq-to-sql-at-stackoverflow-com?rq=1
          • 0
            Странно, ваш комментарий выглядит как возражение. :)
      • 0
        Процедуры могут помочь когда возможностей Linq-провайдеров недостаточно, но заменить Linq, не потеряв темп разработки и\или быстродействие, почти нереально.

        Как у LINQ обстаят дела с:
        1. Хинтами к запросам
        2. Атомарными операциями
        3. Транзакциями
        4. Компиляцией/оптимизацией запроса


        Ну а про масштабируемость — это вообще отдельная песня. Кейсы:
        1. Отсутсвие тестового стенда
        2. Несколько комманд работающих с одной БД


        Я, конечно, не предлагаю переносить BL в БД. Да и мапперами пользуюсь ещё со времён DAO в C++.
        Но для высоконагруженных приложений, такие поблажки могут привести плачевный результат в будущем…
        • 0
          Как у LINQ обстаят дела с:
          Атомарными операциями
          Транзакциями

          В чем отличие одного от другого для вас? С транзакциями в EF все нормально.

          Ну а про масштабируемость — это вообще отдельная песня. Кейсы:
          Отсутсвие тестового стенда
          Несколько комманд работающих с одной БД

          Какое отношение эти два кейса имеют к масштабируемости вообще и к EF в частности?
          • 0
            В чем отличие одного от другого для вас?

            MERGE?

            С транзакциями в EF все нормально.

            По мне, с клиента не хорошо открывать транзакцию. Уж лучше на сервере за ней следить.
            Да и за ошибками SQL'я, лучше в самом SQLе следить.

            Какое отношение эти два кейса имеют к масштабируемости вообще и к EF в частности?

            Разве у вас в комманде, DBA не ругаются в такой неконтролируемый пул запросов?

            Распишу по порядку:
            Отсутсвие тестового стенда
            Есть рабочий код, в который постоянное кол-во обращений. Если мы заходим отрефакторить БД:
            • Делаем/меняем актуальную структуру данных. (Мы можем менять не боясь поломать код. Т.к. мы знаем все запросы и все датасеты внутри БД)
            • Дорабатываем код всех клиентов согласно новой структуре данных
            • Когда все клиенты перешли на новый формат, мы точно узнаем, что старые хранимки больше не используются.


            Несколько комманд работающих с одной БД
            Скажем, одна комманда делает backend, другая frontend. Комманды пересекаются в узком месте и релизы не согласованы.
            Все вместе, они пользуются одним набором хранимок, а выдаваемый код должен быть идентичен на время 2-3 последующих итераций.
            Соответственно, одна комманда меняет таблицы, вьюхи и хранимки так, чтобы не поломать других клиентов.
            • 0
              По мне, с клиента не хорошо открывать транзакцию.

              Эта позиция неверна, потому что только «клиент» (на самом деле — бизнес-слой) знает реальный границы атомарности идущей операции.

              DBA не ругаются в такой неконтролируемый пул запросов?

              Нет.

              А что касается обоих ваших примеров — просто представьте, что вокруг БД есть гарантированный уровень абстракции в виде DAL, и поймите, что все ваши методики к нему равноприменимы.
              • 0
                Эта позиция неверна, потому что только «клиент» (на самом деле — бизнес-слой) знает реальный границы атомарности идущей операции.

                А если клиентов несколько? И много языков программирования? И всё это это должно выполнять целостость операции?

                А что касается обоих ваших примеров — просто представьте, что вокруг БД есть гарантированный уровень абстракции в виде DAL, и поймите, что все ваши методики к нему равноприменимы.

                Я правильно понимаю, что для этого нужен отдельный сервер BL, который будет рулить запросами (SOA)?
                Но это прилично поднимает стоимость разработки…
                А если ещё учесть разноплановость групп разработчиков, то поддержка такого слоя может стоить ещё дороше…
                • 0
                  А если клиентов несколько? И много языков программирования? И всё это это должно выполнять целостость операции?

                  Сервисный слой.

                  Я правильно понимаю, что для этого нужен отдельный сервер BL, который будет рулить запросами (SOA)?

                  Не обязательно. Зависит от решения.

                  Но это прилично поднимает стоимость разработки…

                  Смотря с чем сравнивать. В моем опыте стоимость поддержки адекватного решения на .net ниже, чем стоимость поддержки бизнес-логики в MS SQL.
          • 0
            В чем отличие одного от другого для вас? С транзакциями в EF все нормально.

            Как бы да. Но как бы не везде они приветствуются. Например функции самовосстановления и авто-повторения в случаях сбоя в EFW в условиях транзакций не работают.
        • 0
          1) Хинты в Linq не нужны. Linq не генерирует таких запросов, чтобы их пришлось пинать план хинтами. Если план запроса плохой, то надо смотреть индексы и статистику, а не писать хины. Хинты блокировок тоже не самая лучшая идея, лучше выбрать уровень изоляции правильный для твоего случая и ограничить выборку, чем писать хинты. Но если очень надо, то есть Plan Guide, который может DBA навесить на любой запрос, программисту это категорически не надо делать.

          2) Изменения атомарны, SaveChanges во всех провайдерах выполняется в транзакции, все или ничего, поэтому неконсистентность базы получить нельзя.

          3) Транзакции можно вручную открывать и закрывать во многих провайдерах.

          4) На уровне СУБД каждый запрос компилируется. Во всех распространенных провайдерах есть свой компилятор запроса, который один раз преобразует Linq в SQL и отдает тебе фукнцию, через которую можно результаты получить. Но обычно это доли, которые могут быть важны только при очень большой нагрузке (такой, которую менее 1% увидят в жизни увидят).
          • 0
            Во всех распространенных провайдерах есть свой компилятор запроса, который один раз преобразует Linq в SQL и отдает тебе фукнцию, через которую можно результаты получить. Но обычно это доли, которые могут быть важны только при очень большой нагрузке (такой, которую менее 1% увидят в жизни увидят).

            Ах если бы. При сравнительно сложной иерархии наследования EF может зависать на построении SQL несколько десятков секунд. К сожалению, реальная история.
            • 0
              Это баг конкретного провайдера, и, насколько мне известно, поправленый еще в EF4.
              • 0
                4.3.1 — воспроизводится стабильно.
                • 0
                  Текущая версия 6.1.1 если что.
                  • 0
                    Спасибо, кэп. Вот только (а) в четвертом, как вы написали, не пофиксили и (б) не все проекты можно легко и быстро мигрировать.
                    • +1
                      В текущей версии баг пофикшен, версия 4.3.1 была более двух лет назад, за это время можно было мигрироваться. Если вы этого не сделали, то это не проблема EF или другого провайдера.
                      • 0
                        Вот смигрируемся — и увидим, пофикшена ли. На простых тестах это не подтверждалось.
                        • 0
                          У нас EF 5, и тоже такая проблема наблюдалась. Один большой сложный запрос, как раз таки строящий «объемную» проекцию, генерировался до минуты. В нем было много предусловий, около 15 (т.н. advanced filter на UI, по требованию бизнеса). Причем время увеличивалось в зависимости от количества выбранных условий. Т.е. это не похоже на багу, скорее сложность.
                          Пришлось перенести вcю фильтрацию в память. Запрос стал статическим, т.е. генерируется один раз, потом EF его кеширует. Но время исполнения тоже удручающее.
        • 0
          К слову — EF еще сам по себе тормозной, даже без учета возможной неоптимальности запросов. Он может реально 60мс потратить в запросе — на его построение и обратный мэппинг, когда БД потратит 10мс на сам запрос.

          Но если проект активно развивается, и схема БД активно меняется, то проблемы с производительностью отбиваются скоростью и удобством разработки. По крайней мере, в связке .NET+MSSQL, из-за крайнего уродства встроенного языка и инструментов разработки под MS SQL. Не уверен что с PostgreSQL и Oracle будет такой же эффект.

          В любом случае, к моменту когда проект поедет, и потребуется добавить скорости, у тебя будет возможность переписать узкие места на SQL. Скажем, воткнуть light ORM параллельно с EF — как это сделали на stack overflow. Но к тому моменту у тебя будет уже все работать, схема БД устаканится, а возможно даже будет больше денег и разработчиков.
          • 0
            А я именно на эту тему придумал решение, как обойти «тормознутые» части EF, и при этом оставить профит быстрой разработки.
            здесь
          • 0
            Самая тормозная часть — Change Tracking, выключите его и время упадет раз в 5-6
            • 0
              Влияет ли это на запросы с проекциями?
              • 0
                Нет, не влияет.

                Вообще в EF есть кеш запросов, один и тот же по структуре запрос он полностью «компилирует» один раз, потом кеширует, потом выполняется только один обход дерева выражения для вычисления хеша. Если каждый раз EF компилирует запросы по 10 сек, то это уже баг, надо его засабмттить в трекер.

                Кстати были performance баги в 6.0 и 6.1, имеет смысл перейти на 6.1.1

    • 0
      Они органично вписываются и дополняют EFW в нужных местах.
      Например, Table Valued Functions.
      Или, если вам необходимо использовать хинты SQL или другие низко-уровневые вещи.
      Мы таким образом прикручивали FullTextSearch к EFW.
      • 0
        Добавлю ещё, что т.к. EFW делает параметризированные запросы, то есть вероятность получить плохой план выполнения и жить с ним всю жизнь приложения. Речь идет о том, что SQL сервер строит план для первого исполнения исходя из текущих параметров. А потом параметры изменяется относительно статистики таблиц, а план — уже нет. И тут нужен with recompile. А в EFW такого просто нет. И тут уже без хранимок не представляю даже — как выкрутиться. Реальный случай.
        • 0
          Речь идет о том, что SQL сервер строит план для первого исполнения исходя из текущих параметров

          Что-то там MSSQL допиливал, когда выяснилось, что LINQ2SQL отправляет (N)VarChar разной длинны в заваисимости от длин строк.
          Так что в системе собирался огромный массив планов выполнения исходя из всех длин строк всех входящих параметров.
          И тут нужен with recompile. А в EFW такого просто нет. И тут уже без хранимок не представляю даже — как выкрутиться. Реальный случай.

          У нас DBA ругались, что некоторые хранимки использовались с флагами.
          И первый флаг генерил словарь, а все остальные — динамические датасеты. Почему-то им такая хранимка не нравилась с вопроса производительности…
      • 0
        Они органично вписываются и дополняют EFW в нужных местах.

        По мне, так главное сущьности не смешивать.
        Чтобы не получить использование таблицы напрямую (из ORM) и из хранимки.

        Или, если вам необходимо использовать хинты SQL или другие низко-уровневые вещи.

        Разве with(nolock) такая низкоуровневая вешь? По мне, так для пользователя (не сотрудника) в самый раз. Иногда даже и для сотрудника сгодится.
        • 0
          with nolock — аналог выставляется и через уровень изоляции транзакции средствами EFW.
          А вот, скажем, SELECT… FOR UPDATE — уже опачки.
          • 0
            А в чем проблема написать SQL, если нужен именно select for update? Почти все провайдеры позволяют.
        • 0
          Самое сложное в nolock — это объяснить «пользователю», какие данные он может получить в системе с большим количеством одновременно работающих клиентов.
          • 0
            Самое сложное в nolock — изучить что-то кроме nolock. Сегодня есть READ COMMITED SNAPSHOT, который в 99,9% случаев позволяет не писать nolock. И его по умолчанию задает EF когда создает базы.
            • 0
              Оооо, снепшоты. Снепшоты — это круто, но они неизбежно медленнее nolock. И ими тоже надо уметь пользоваться (в частности, явно открывать транзакции).
              • 0
                Не путай Read Commited Snapshot и Snapshot isolation level.
                • 0
                  А вы думаете, row versioning в RCSI как-то сильно иначе сделан и не дает нагрузки на производительность? Или, скажем, поведение системы под транзакциями не начинает отличаться?
                  • 0
                    SNAPSHOT ISOLATION LEVEL создает по сути слепок данных на момент запроса и удерживает слепок до конца транзакции. Например при чтении таблицы будет создана копия всей таблицы. На самом деле чуть хитрее, копия будет сделана не сразу, а только при записи, но при сильной конкуренции ситуация будет близка к этой.
                    Это дает изолированность транзакций, то есть нет фантомов, неповторяемых и грязных чтений, но не дает сериализуемости.

                    READ COMMITED SNAPSHOT заставляет при записи не вешать лок, а делать копию строки, и при коммите заменять основную версию. При чтении никаких локов и копирование строк не происходит. Но работает только на уровне изоляции READ COMMITED, то есть позволяет неповторяемое чтение и фантомы.

                    Механизм версионирования внутри одинаковый, а применение разное и эффект разный.

                    Проблемы с обоими SNAPSHOT могут быть при высококонкурентном доступе к одним и тем же данным.
                    • 0
                      Я не спорю, что поведение разное, я просто констатирую факт, что оба этих механизма в неумелых руках не лучше nolock.
                      • 0
                        RCS все таки лучше nolock
                        1) Его не надо писать руками
                        2) Уровень изоляции таки read commited

                        Именно поэтому EF ставит RCS для новых баз.

                        Я кстати видел dirty read баги в живой системе, которую «оптимизировали» с помощью nolock. Причем такие баги диагностировать почти нереально ибо весь код отрабатывает в этом случае корректно, а по сути бага была в том что чтение происходило между вставкой строк в одном insert операторе.
  • +1
    Ещё статья по теме: 10 мифов о LINQ.
  • 0
    Касательно LOB — а не лучше ли делать, чтобы в базе они лежали в отдельной таблице, где только LOB и foreign ключ?
    • 0
      Это не исправит ситуацию в целом.

      Для примера — таблица товара имеет поле Description nvarchar(max). По умолчанию оно попадает на отдельные страницы данных в SQL Server, поэтому если не укажешь в проекции то вытягиваться не будет.

      С другой стороны на странице редактирования товара ты захочешь использовать Model Binding, которому понадобится целый объект вместе с полем Description, и ты настроишь маппер так, чтобы целый объект у тебя формировался с помощью Join. Но тогда такой же целый объект будет формироваться при выводе каталога, если ты не напишешь проекцию.

      Вообще пытаться оптимизировать структуру базы до того как оптимизированы запросы — контрпродуктивно.
      • 0
        Интернеты говорят следующее:
        By default, nvarchar(MAX) values are stored exactly the same as nvarchar(4000) values would be, unless the actual length exceed 4000 characters; in that case, the in-row data is replaced by a pointer to one or more seperate pages where the data is stored.

        То есть со строками такой проблемы быть не должно, по крайней мере в MS SQL Server.
        • 0
          То, что здесь процитировано, говорит лишь о том, что страница с данными не хранит сам CLOB — вместо него хранится указатель на CLOB в куче. Это увеличивает количество записей на страницу и помогает быстрее сканировать страницы при операциях, не затрагивающих сами клобы — то есть является по сути внутрисерверной реализацией идеи хранить клобы в отдельной таблице. Однако, от бесполезного вычитывания, передачи по сети в слой приложения и инициализации неиспользуемых полей, о котором говорится у топикстартера, это не спасает.
          • 0
            Как раз наоборот, говорится, что по умолчанию данные хранятся in-row, и лишь при достижения 4k они начинают себя вести как LOB.

            Я не совсем понимаю, почему, если известно, что используется EF, не предьявить требования к базе данных? Например, кроме упомянутого вынесения LOB в отдельные таблицы, желательно, чтобы во всех таблицах был identity, не считая таблиц many-to-many ассоциаций.

            Из моего опыта оптимизаций, тормозят больше всего как раз все эти фичи EF, в т.ч. проекции. Тормозят, правда, у потребителя, не на бд. В разы быстрее работает полная материализация, и раскладывание/преобразование/проецирование в памяти. В моем случае я добился 4.6х разницы в производительности. Опять же, как в статье упомянуто, база данных может быть блокирована на время материализации. Другими словами, я бы еще посмотрел на конкретный случай, что затратнее, долгая блокировка или вычитка лишних полей.
            • 0
              Я имел в виду то, что независимо от способа хранения, если вы затребуете SELECT * FROM XYZ, где * содержит CLOB или BLOB, то SQL server вытащит этот самый блоб и будет пропихивать его вам в резалтсете — и это и дает чудовищный оверхед по I/O Cost и network traffic, не говоря уже о затратах слоя приложения.

              Помимо этого, In-row хуже с точки зрения плотности упаковки записей на страницу — а это очень важный параметр, в конечном итоге определяющий скорость выборки данных при любых способах (scan, seek, lookup, range), определяемых планом. Out-of-row можно enforce для строк любой длины, чтобы всегда можно было рассчитывать на плотную упаковку записей — msdn.microsoft.com/en-us/library/ms173530.aspx
        • 0
          Там все хитрее, одна запись должна целиком влезать на страницу, то есть быть не длиннее 8000 с мелочью байт. Если у тебя несколько полей nvarchar(max) и во всех достаточно длинный текст, то они все пойду в отдельную страницу. Поэтому с короткими строками такой проблемы нет вообще, а с длинными, такими как описание товара, проблема встает в полный рост.

          В идеале запрос на список чего-либо должен полностью покрываться индексом, чтобы вообще не требовалось обращаться к страницам данных. Но в индекс включать nvarchar(max) колонки — крайне плохая идея.

          Поэтому в общем случае вертикальное секционирование проблему не решает.
      • 0
        Вообще пытаться оптимизировать структуру базы до того как оптимизированы запросы — контрпродуктивно

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

        Структура базы определяется сущностями автоматизируемого процесса и операциями над ними. Далее идет итерация оптимизации по объему и скорости работы — еще до фактического написания слоя приложения — просто через анализ требований к работе с данными. Вопреки утверждениям «практиков», в большинстве случаев можно еще до фактического программирования сказать, в каком месте нужно во имя скорости отойти от НФ, где использовать массовые операции, где — специфические индексы, вычислимые поля, констрейнты итд.
        При этом качество запросов к БД (которые еще не написаны), естественно, априори нужно подразумевать хорошим.
        Тогда во время нагрузочного тестирования модификации не будут драматическими, а будут касаться лишь тех вещей, которые по ошибке были продуманы не так глубоко, или просто забыты.

        Если же надеяться на то, что «минорные» проблемы на стейджинге (table scan в основных операциях, дедлоки, конфликты одновременной модификации итд) можно будет подлечить, набив памяти и добавив ядер в продакшен-сервер, или добавив каких-нибудь «магических» индексов (которые даже изобретать не надо — MS SQL студия теперь ведь умнее разработчиков — сама подсказывает, что сделать надо) — то вместо легкого и прозрачного бакенда вы получите утюг, который разгоняется только полной перепиской с нуля.

        Аналогия тут с авиастроением — никому ведь не приходит в голову проектировать силовую схему планера из стали, и только убедившись в «неоптимальности конструкции», пытаться сначала высверлить отверстия в силовых конструкциях для облегчения, и лишь затем, пересобрав весь «солюшен», заменить материал на конструкционный алюминий.
        • +1
          Про проектирование все правильно написано, если предположить что все известно заранее (редко, но бывает). Но как это помешает программисту написать кривой запрос, как в посте? И самая лучшая схема не поможет запрос соптимизировать если он криво написан.

          Лечить table scan добавлением памяти и процессоров — это пять. Table Scan, особенно если большая таблица, не лечится добавлением процессоров, да и добавление памяти не сильно помогает. «Магические» индексы тоже не помогают если предикат кривой. В итоге сначала все равно придется править запросы, а потом уже смотреть на схему.

          Аналогии по умолчанию лживы. Например конструкторские дисциплины опираются на физику, а в разработке ПО влияние физики незаметно мало отличается от нуля. А многие другие факторы слишком субъективны и неформализуемы.
          Фактически есть только 3 измеряемые величины в разработке ПО — кол-во строк кода в проекте, продуктивность (кол-во строк кода в единицу времени), и плотность дефектов (кол-во дефектов на 1000 строк). Вот это «физика» разработки ПО, все остальное субъективно. При такой «физике» строить закономерности сложно.
          • +1
            Лечить table scan добавлением памяти и процессоров — это пять
            — к сожалению, подобная практика мне встречалась и продолжает встречаться даже в очень серьезных системах — DBA пытаются изо всех сил исправить косяки проектирования, чем только могут — ставя 8-ядерные выделенные сервера с 64 Гб памяти только потому, что в тестовой конфигурации в ключевых таблицах было по 10^5 строк, а в продакшене — 10^7, а под лихой запрос разработчика, странслированный EF, не может быть использован ни один существующий или какой-нибудь новый индекс — в частности, и из-за корявости предикатов.

            Аналогии по умолчанию лживы
            — ваши измеряемые величины в разработке ПО — это метрики эффективности самого процесса разработки. А я говорю о метриках эффективности работы самой создаваемой системы, то есть результата этого процесса. И они больше зависят от качества проектирования, чем от качества ведения процесса разработки (если, конечно, интенсивность багов остается в разумных пределах).

            При этом процесс проектирования программных систем необязательно должен опираться на математически строгую науку — в большинстве случаев хорошие и очень хорошие результаты получаются при примемении эвристик опытного проектировщика, хотя как раз бакенды очень хорошо покрываются теорией массового обслуживания. Так же, как для самолетов нужно избегать тяжелых материалов в размерных конструкциях, для бакендов нужно избегать статистически частых линейных операций над большими наборами данных. Для того, чтобы это сделать, не нужно знать ни физики, ни математики. Дальше желательно разносить потоки с существенно разным отношением R/W в разные объекты блокировки (в данном случае, таблицы). Минимизировать transaction footprint — то есть время-сечение объектов, которые затрагиваются транзакцией (на которые наложены локи) в течение бизнес-запроса.
            Не выбирать ненужное (сюда varchar(max) out-of-row, included-колонки в индексах, и понимание преимуществ, которые дает плотность упаковки записей на страницу). Пакетность запросов — реализация преимуществ, которые дает масссовая обработка записей, и которая начисто отрезается при модификации данных средствами EF. Использование специфических возможностей DB engine — сюда идут OUTPUT-клаусы, встроенная атомарность, хинты, констрейнты, дефолтные функции, частичные индексы, синтезированные уникальные ключи, нереляционные джойны, генераторы данных итд.

            Что-то в конце получилось сумбурно, но мысль такова, что DB engine обладает мощным набором *специфических для массовой обработки данных* фич, количество которых понемногу увеличивается от версии к версии (причем разработка этих фич совсем не бесплатна, а эффективность по крайней мере >= 0). Поскольку EF неспособен использовать значительную часть этих фич, а они продолжают существовать и развиваться — значит есть другой, более эффективный путь построения систем, без использования EF, для которого собственно множество этих фичей и продолжают поддерживать и наращивать.

            • 0
              есть другой, более эффективный путь построения систем, без использования EF,

              Точнее сказать «есть системы, для которых совокупная стоимость разработки и поддержки без использования EF окажется ниже, чем с EF».
              • 0
                Надо добавить «в теории», потому что на практике сильно зависит не от технологии, а от кучи других факторов.
                • 0
                  И в практике тоже. Просто расчет совокупной стоимости в теории и в практике слегка отличается.
            • 0
              DBA во многих случаях не могут поправить запросы, поэтому и лечат table scan как могут. Это кстати не зависит от EF.

              А по поводу невозможности — habrahabr.ru/post/230623/, миф №6
  • +1
    Такие ужасы делают не только на linq, например человек не слышал про join и делает это в коде выборками из нужным таблиц и перебором в цикле, дабы соединить по ключу таблицы (лично видел такой код).
  • –1
    Ваши советы тоже хороши.

    .Where(x => x.CreatedOn >= DateTime.Today && x.CreatedOn < DateTime.Today.AddDays(1))
    


    Обратите внимание
    stackoverflow.com/questions/4146300/linq-to-entityframework-datetime

    С моей точки зрения правильно тут предварительно выделить две переменные для границы даты/времени, инициализировать их до запроса и потом уже в бой.
    • 0
      Во что превратиться AddDays зависит от провайдера. EF например не поддерживает, но можно заменить на DbFunctions.AddDays, а в Linq2SQL я использовал DateTime.AddDays в запросах. В точки зрения SQL — одинаково, поэтому нет смысла писать больше.
      • –1
        В своих сообщениях вы всё валите на «конкретных провайдеров». А конкретный провайдер совершенно не обязан уметь DateTime.Today или DateTime.AddDays().
        Кроме того, и недочет, который вы исправляете (реализация EntityFunctions.TruncateTime ) тоже связан с конкретным провайдером.

        Или крест сними или трусы одень (с).
        • 0
          1) Все известные провайдеры поддерживают DateTime, если не полностью, то частично
          2) Все известные провайдеры поддерживают функции. Суть не в конкретной функции, а в том что функции не будут оптимизироваться во многих случаях.

          ЗЫ. Не одень, а надень.
          • 0
            Вот потому некоторые ф-ии и выделены в EntityFunctions.* (DbFunctions.*) — что этот арсенал точно будет поддерживаться.
            Всё остальное — не гарантировано.
            В конце концов вы и сами провайдера расширить можете, но это не публичный функционал. У остальных он не сработает.

            Я вам специально дал ссылку, чтобы не быть голословным. Принимать ли факты к вниманию — дело ваше.
            • 0
              Вообще ничего не гарантированно, что захотят авторы конкретного провайдера, то и будут делать. По сути единственное требование к провайдерам — семантика Linq запроса к базе должна быть похожа на семантику Linq запроса к объектам. Все остальное на усмотрение авторов.
  • 0
    WHERE cast(CreatedOn as Date) = cast(GETDATE() as Date)

    Естественно ни одни индекс не сможет оптимизировать такую выборку и будет полный обход таблицы.

    Вы удивитесь, но в MS SQL Server обхода таблицы не будет — посмотрите план выполнения, будет Index Seek (я сейчас говорю не про XXX to Database, а про чистый T-SQL который вы написали).
    • 0
      Конкретно этот запрос не проверял, в зависимости от провайдера может разный запрос получится. Поэтому рассчитывать, что не будет скана или его можно будет убрать — нельзя.
      • 0
        Дело в том, что Вы разложили «неэффективный» предикат в «эффективный» путем обозначения границ. Таким же образом это сделал бы оптимизатор запросов.
        Вы это сделали с рассчетом на то в какой именно запрос выльется. С другой стороны Вы же говорите, что закладываться на то, каким получится запрос нельзя, ибо провайдер непредсказуем. Нестыковочка получается…
        • 0
          Нельзя закладываться во что преобразуются функции, это надо проверять и смотреть в документации, а если не писать функции, то Linq ничего в предикате не сгенерит.

          Вообще если из А следует Б, то это не значит что из Б следует А. ;)
    • 0
      В разных версиях EFW получаются разные запросы. В ранних (4) вообще поле приводилось к строке. В итоге один и тот же код может исполниться с разным результатом.

      Вообще, засада может случиться где её совсем не ждешь. Например, бывают глюки с запросами, где используется enum.
      Поэтому мы просто запускаем монитор SQL сервера и отлавливаем необычно тяжелые запросы. И предметно по ним уже отрабатываем.
  • +1
    Метод в примере я бы написал как два:
    //в DataContext или каком-то еще DAL:
    public IQueryable<Document> GetDocumentsToday()
    //в классе DocumentDto
    public static DocumentDto[] QueryFrom(IQueryable<Document> query)
    


    Таким образом, инкапсулируется условно сложный фильтр в GetDocumentsToday, при этом клиентскому коду остается гибкость: можно дофильтровать еще по какому-то признаку — например достать первые 100 документов. И достать потом что-то еще тем же запросом — например count() для количества страниц.

    По вкусу GetDocumentsToday можно еще переписать в public static IQueryable<Document> AddedToday(this IQueryable<Document> query). Как еще вариант — сделать public Expression<Func<Document, bool>> AddedTodayFilter, или Expression<Func<Document, bool>> GetAddedTodayFilter().

    В общем не надо бежать побыстрее оборачивать EF фасадами, возвращающими IEnumerable. Нужно как можно большую часть DAL и его API строить на манипуляции IQueryable.
    • –1
      Здравая мысль, но клиенты бизнеса должны бы оперировать бизнес-объектами. Поэтому IQueryable query в public методе выглядит некрасиво. Просто потому, что в контрактах уйдет вся схема БД с навигационными св-вами и т.д.
      • 0
        А это не бизнес, это DAL. А бизнес еще на уровень дальше.
        • +1
          Не факт что это DAL — это может быть и бизнес. Например условие, которое проверяет что-нибудь типа order.Status == OrderStatus.Closed || order.Status == OrderStatus.Rejected — это часть бизнес-логики. Но это условие нужно использовать именно поверх IQueryable — иначе никак.

          Я не вижу ничего плохого в бизнес-логике поверх IQueryable. Задача DAL — предоставить API для работы с данными, чем IQueryable для этого плох?
        • 0
          Я правильно понимаю, что у вас
          1. Используется EFW/LINQ2SQL/что-то подобное
          2. Выделен самостоятельный слой доступа к данным
          3. Он не смешан с бизнес логикой

          ?

          • 0
            «У нас» — по-разному, но в общем, именно так, как вы описали.
            • 0
              Можете тогда написать примерное количество публичных методов в DAL?
              Что он такого предоставляет слою бизнеса, кроме открыть реп, прочитать через IQueryable, закрыть реп?

              • 0
                Методов много (десятки на сложные репозитории).

                Слою бизнеса он предоставляет изоляцию (например, в DAL может быть метод Guid Add(name, data), который прячет все операции по созданию объектов, их наполнению, и доставанию идентификатора, создаваемого в БД).
      • +1
        Непонятно как у тебя выглядит разбиение на слои, что подразумевается под клиентами бизнеса. У меня, например, большая часть бизнес- и UI-логики — она про то что мы хотим показать пользователю. Никак не получится эту логику описать поверх IEnumerable — каждая операция будет доставать половину БД.

        Ну, либо можно делать вид что IEnumerable GetTopTodayOrdersWithOrderLinesCountAndSummariesToDisplayOnHomePage() — это часть DTO, а не большой неатомарный кусок бизнес-логики переехавший в DAL.
  • 0
    Прошу прощения за некропостинг, но мне кажется, стоило бы упомянуть bulk-вставки/удаления/обновления и нюансы треканья состояния сущностей (что будет актуально для пользователей EF).

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