Комментарии 12
Если навигационные свойства не загружать принудительно при помощи Include()
и не обращаться к ним, то они и не будут загружаться из БД. Когда полученная DAL модель не используется целиком, а только некоторая ее проекция (например родительская сущность + 1 вложенная коллекция), остальные вложенные коллекции не вычитываются из БД.
Лучше — это выбрать именно по c.parentpostid
и заполнить коллекцию уже в памяти приложения? Да, вы правы это будет ЕЩЁ быстрее. Но те крошки которые мы соберем в этом случае имеют trade off — сильно сложный и разветвлённый код на DAL слое.
Выполнить 2 запроса параллельно в ef core нельзя, DbContext не потокобезопасный. Насколько я помню он даже select не позволяет выполнить пока не закончится предыдущий запрос. Технически можно было бы создать новый и выполнить второй запрос из него, но опять же — очень сильные приседания, с неизвестным результатом. Когда будет задача ловить миллисекунды — обязательно попробуем :-)
Выполнить 2 запроса параллельно в ef core нельзя, DbContext не потокобезопасный
Эта проблема действительно с успехом решается инстанцированием второго контекста
Безусловно вы правы.
Давайте рассмотрим pros-contras этого решения?
Contras:
- Невозможно обеспечить целостность данных там где это критично. Транзакция будет существовать только в своём контексте
- Ещё один мультипликатор в расчёт степени параллелилизма. То есть до этого были параллельные запросы от одного пользователя, несколько пользователей одновременно, так еще и бэкенд сам генерирует параллельные запросы к БД на каждый единичный входящий запрос.
- Кастомный код репозитория под каждую сущность. То есть даже CRUD надо писать ручками.
Pros:
- быстрее не больше чем на выполнение базового запроса выборки из родительской таблицы.
Какое применение у такого подхода может быть? Ну что-то где критически важна латентность, где данные нужны максимально быстро и при этом есть гарантия не положить контроллер диска и не заткнуть СУБД. А нужна ли в таком случае РСУБД как первичный источник данных? Может посмотреть на что-то не реляционное и in-memory? Redis, например.
Буст с 34 секунд до 5,5 мс уже неплох, параллельные запросы дадут еще -1-2 мс, но читаемость и поддерживаемость кода будут куда хуже.
Кастомный код репозитория под каждую сущность. То есть даже CRUD надо писать ручками.
А вы CRUD не ручками пишете? Или всегда вытаскиваете только атомарные сущности? Или много лишнего за собой тащите?
Не всегда, иногда удобно выделить базовую абстракцию и потом подсовывать в нее разные IQueryable<>
. Это отлично работает для атомарных сущностей или вытаскивания "целиком".
Для вытаскивания проекции — безусловно лучше наклепать отдельный метод, чтобы не тащить лишку. Но тут вопрос альтернативной стоимости — что будет в итоге проще поддерживать.
Не факт. Специально для вас только что погонял бенчмарк на той же модели и получил такой результат:
AsSplitQuery 2 3.814 ms 0.3040 ms 0.8917 ms
CustomQueries 2 3.803 ms 0.1896 ms 0.5441 ms
AsSplitQuery 5 3.934 ms 0.2411 ms 0.7070 ms
CustomQueries 5 3.784 ms 0.1974 ms 0.5758 ms
AsSplitQuery 10 3.952 ms 0.2094 ms 0.5941 ms
CustomQueries 10 4.531 ms 0.2756 ms 0.8083 ms
AsSplitQuery 100 6.610 ms 0.2997 ms 0.8696 ms
CustomQueries 100 6.751 ms 0.3558 ms 1.0492 ms
Разница на уровне статистической погрешности.
Вторая колонка — количество элементов во вложенной коллекции
Пока еще не приходилось использовать, но, возможно, придется. Кстати, предыдущий комментатор указал на способ с двумя параллельными запросами, что отлично сработало бы, как мне видится в Вашем конкретном случае. Контекст реально нельзя использовать в нескольких параллельных операциях (https://docs.microsoft.com/en-us/ef/core/miscellaneous/async). Но можно создать для каждого отдельного запроса свой контекст (в случае поста и комментариев это не должно нарушить согласованности данных, а создание контекста почти ничего не стоит). Эти два запроса параллельно отправить в БД. Затем дождаться через
WhenAll
или WhenAny
. Думаю, это будет быстрее работать, чем .AsSplitQuery()
, так как эта функция делает два последовательных запроса. Когда много параллельных задач к БД это тоже зло, но это отдельная тема.На здоровье!
Да, второй запрос без JOIN действительно отработает быстрее, чем запрос с JOIN, хотя есть у меня подозрения, что умный оптимизатор сведет разницу на нет.
Тут другой момент, кроме того что код становится жутко не типовым и требует дополнительных приседаний (см. ниже), его можно запустить параллельно только если мы знаем ID, по которому и сделали бы JOIN. А это не всегда так на момент начала выполнения запроса. Что если мы выбираем не по условию p.id = 42
, а по условию p.rank > 100500
? В этом случае никакой параллельности и без JOIN-ов мы не получим.
Про приседания. Пусть у нас этих сущностей не 2 и не 8 (как в бенчмарке), а 100? При этом у каждой своя структура, свои коллекции. Пока не представляю, как можно написать простой шаблонный репозиторий, в который мы бы подкладывали только IQueryable<>
. Тут и утонуть можно.
Что касается параллельного создания контекстов — операция не совсем бесплатная, это дополнительный коннект к БД. Не проводил измерений, но сдаётся мне создание нового коннекта обходится недёшево, иначе зачем бы их пулировать? Ну и да, много параллельных запросов плохо сказываются на работоспособности СХД, можно очень сильно заткнуть очередь диска.
Не говорю, что split queries — серебряная пуля, но решение красивое и здорово, что оно теперь внутри EF Core.
Неплохо, на EF6 загружали руками с помощью DbEntry.Collection и DbEntry.Reference. Это намного быстрее, чем построение сложного запроса.
В ef core так тоже можно! Видимо SQL получится таким же, кроме случаев когда выбирается коллекция родительских элементов, тут нас догонит проблема N+1
Разделённые запросы в EF Core