← Назад ко всем статьям

Dapper vs EF Core: практическое сравнение производительности в .NET

Опубликовано

Версия этой статьи в научном журнале доступна здесь: Dapper vs EF Core: A Practical Performance Comparison in .NET

Релиз benchmark с исходными материалами доступен здесь: efcore-vs-dapper v1.0.0

Выбор технологии доступа к данным в .NET-приложении - это не просто вопрос личных предпочтений. Он влияет на latency, аллокации памяти, нагрузку на garbage collector, сопровождаемость, а иногда и на стоимость инфраструктуры под нагрузкой.

Во многих enterprise-системах на .NET выбор часто сводится к двум популярным подходам:

  • Entity Framework Core: полноценный ORM, который дает продуктивность, LINQ-запросы, change tracking, миграции и высокоуровневую модель persistence.
  • Dapper: легковесный micro-ORM, который оставляет вас ближе к SQL и фокусируется на быстром mapping с минимальной абстракцией.

Обычно ответ звучит так: “EF Core удобнее, Dapper быстрее”.

Но насколько велика разница? И в каких сценариях она действительно имеет значение?

Чтобы ответить на это, мы сравнили EF Core и Dapper на PostgreSQL, .NET и BenchmarkDotNet в нескольких реалистичных сценариях: чтение, JOIN, обновления и параллельные запросы.

Тестовое окружение

Benchmark выполнялся против PostgreSQL 17.6 с использованием .NET 10 и BenchmarkDotNet. База данных пересоздавалась в контролируемом окружении, а тестовые данные генерировались детерминированно с фиксированным seed.

Цель была не в том, чтобы измерить изолированную синтетическую micro-operation, а в том, чтобы оценить более реалистичный end-to-end путь доступа к данным:

  • построение запроса;
  • round-trip до базы данных;
  • materialization строк;
  • mapping в объекты;
  • аллокации памяти.

Тестовая схема представляла небольшой e-commerce domain с products, orders и order_items.

Database schema with products, orders, and order_items tables

Рисунок 1. Схема базы данных с таблицами products, orders и order_items.

Нагрузка включала чтение одной строки по primary key, чтение с фильтром, paginated reads на таблицах разного размера, выборку orders с большим количеством JOIN, простые обновления, сложные обновления и параллельные чтения с высокой степенью concurrency.

Для сценариев чтения EF Core был настроен на no-tracking queries, чтобы убрать лишние накладные расходы change tracking. EF Core compiled queries также тестировались как отдельный вариант.

Во время review исходного кода для этого исследования мы также нашли и сообщили о лишней операции nullable-conversion в асинхронном query path Dapper. Это наблюдение упоминается как деталь реализации и не включено в оценку benchmark.

Dapper GitHub Issue #2184: Unused value in QueryAsync

Почему аллокации важны

Latency важна, но это не единственная метрика, которая имеет значение в managed runtimes вроде .NET.

Если слой доступа к данным выделяет больше памяти на запрос, эту память в итоге должен очистить garbage collector. При стабильной высокой нагрузке это может увеличить частоту GC и ухудшить tail latency.

Другими словами, даже когда два подхода выглядят близко по среднему времени выполнения, вариант с меньшими аллокациями может вести себя предсказуемее в production.

Поэтому benchmark измерял и время выполнения в микросекундах на операцию, и managed memory allocations в KB или MB на операцию.

Простое чтение: у Dapper небольшое, но заметное преимущество

В сценарии поиска одного product по ключу Dapper показал меньшее время выполнения и меньшие аллокации памяти по сравнению с базовым EF Core.

EF Core compiled queries улучшили read path, но не устранили разницу в аллокациях полностью.

SelectOneProduct execution time and memory allocation comparison

Рисунок 2. Время выполнения SelectOneProduct и аллокации памяти для Dapper, EF Core и EF Core compiled queries.

Это важный результат, потому что простые чтения часто встречаются в backend-сервисах. Но разница здесь не драматичная. Для многих бизнес-приложений одного этого результата недостаточно, чтобы заменить EF Core на Dapper.

Более интересные результаты появляются, когда растет сложность запросов.

Массовые чтения: разрыв уменьшается с ростом объема данных

При выборке большого количества products относительная разница между Dapper и EF Core становилась меньше по мере роста общего времени выполнения.

Dapper все еще оставался быстрее в benchmark, но разрыв был менее заметным по сравнению с более сложными сценариями.

SelectNProducts execution time growth by selected record count

Рисунок 3. Рост времени выполнения SelectNProducts при увеличении числа выбранных записей.

Это говорит о том, что для прямолинейных bulk reads работа базы данных и передача данных могут доминировать в общей стоимости операции. ORM overhead все еще существует, но становится меньшей частью общей операции.

JOIN-heavy запросы: Dapper выходит вперед

Самая сильная разница появилась в запросах с большим количеством JOIN.

Benchmark выбирал orders вместе с order items, увеличивая размер object graph, который нужно было materialize в памяти.

Именно здесь у EF Core появляется больше работы. Ему нужно преобразовать LINQ, materialize entities, обработать relationships и управлять внутренним состоянием. Даже с включенным no-tracking здесь остается больше абстракции по сравнению с прямым SQL mapping.

Dapper, напротив, выполняет явный SQL и мапит строки в простые объекты с меньшим количеством внутренней инфраструктуры.

SelectNOrders execution time comparison for JOIN-heavy queries

Рисунок 4. Время выполнения SelectNOrders для Dapper, EF Core и EF Core compiled queries при разных значениях ItemsPerOrder.

В репрезентативном JOIN-heavy сценарии преимущество Dapper по времени составило около 51.2%.

Это не означает, что EF Core плохой. Это означает, что большие object graphs и JOIN-heavy reads - именно тот тип нагрузки, где overhead абстракции становится видимым.

Сложность запроса и speedup

Чтобы лучше показать связь между объемом данных и сложностью запроса, мы также использовали heat map ускорения.

Коэффициент speedup рассчитывался так:

EF Core time / Dapper time

То есть значение 2.0 означает, что EF Core в этом сценарии занял примерно в два раза больше времени, чем Dapper.

Speedup heat map across data volumes and ItemsPerOrder values

Рисунок 5. Коэффициент speedup для разных объемов данных и значений ItemsPerOrder.

Heat map делает паттерн заметнее: Dapper обычно выигрывает больше по мере роста сложности запроса, особенно когда нужно соединять и materialize больше строк.

Параллельные чтения: аллокации становятся важнее

Benchmark также включал сценарий чтения с высокой concurrency и degree of parallelism 128.

В этом случае время выполнения у EF Core и Dapper было почти одинаковым. Преимущество Dapper по времени составило всего около 0.5%.

Но аллокации памяти показали другую картину.

Dapper выделял меньше памяти под contention, что может быть важно в реальных сервисах, где одновременно обрабатывается много запросов.

Parallel memory allocation comparison at DOP 128

Рисунок 6. Аллокации памяти при параллельной выборке с DOP = 128.

Это один из ключевых выводов: performance - это не только средняя latency. Давление аллокаций может влиять на garbage collection, разброс response time и стабильность production.

Summary результатов

Benchmark использовал комбинированный performance index, который учитывал и время выполнения, и аллокации памяти. Этот индекс давал преимущество подходам, которые были не только быстрее, но и эффективнее по памяти.

ScenarioDapper Time AdvantagePI DapperPI EF Core Compiled
SelectOne simple8.4%1.51.3
SelectOne with filter16.4%1.661.43
SelectN, 1000 records6.7%1.161.03
SelectN with JOIN51.2%2.280.97
UpdateOne simple31.6%1.971.28
UpdateOne complex48.3%2.51.3
Parallel query, DOP = 1280.5%1.121.0

Наибольшие преимущества Dapper появились в JOIN-heavy выборках, complex updates и parallel workloads, чувствительных к аллокациям.

Что это значит для архитектуры

Вывод не в том, что нужно “всегда использовать Dapper”.

Более правильный вывод:

Используйте подход к доступу к данным, который соответствует workload.

EF Core - сильный выбор, когда productivity, maintainability, миграции, rich domain modeling и change tracking важнее, чем борьба за каждую аллокацию.

Dapper - сильный выбор, когда в приложении есть latency-sensitive paths, большие read models, JOIN-heavy queries, высокое allocation pressure или explicit SQL, который нужно жестко контролировать.

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

Например:

  • Использовать EF Core для administrative CRUD, внутренних инструментов и business workflows, где сопровождаемость важнее.
  • Использовать Dapper для hot read paths, reporting queries, high-throughput endpoints и complex SQL, где нужен полный контроль над формой запроса.

Такой hybrid approach часто реалистичнее, чем принудительно использовать одну технологию во всей системе.

EF Core compiled queries помогают, но только частично

EF Core compiled queries уменьшили overhead в некоторых read-сценариях, но не изменили принципиально профиль аллокаций.

Причина в том, что query compilation - только одна часть стоимости EF Core.

Остаются и другие затраты:

  • materialization entities;
  • обработка relationships;
  • настройка change tracking;
  • state management;
  • abstraction overhead.

Compiled queries полезны, но это не magic switch, который превращает EF Core в Dapper.

Dapper быстрее, но не бесплатен

Dapper дает больше контроля, но этот контроль идет вместе с ответственностью.

При использовании Dapper разработчики должны самостоятельно управлять корректностью SQL, parameterization, дисциплиной mapping, изменениями схемы, дублированием query logic, transaction boundaries и согласованностью между SQL и application models.

В EF Core многие из этих задач берет на себя framework. В Dapper они остаются ответственностью команды.

Поэтому архитектурный trade-off понятен:

Dapper может снизить runtime overhead, но EF Core может снизить development overhead.

Правильный выбор зависит от того, какая стоимость важнее для вашей системы.

Итоговый вывод

В нашем benchmark Dapper стабильно уменьшал managed allocations и часто улучшал mean latency. Наибольшая польза проявилась в сценариях получения сложных object graphs и обновлений.

EF Core compiled queries улучшили часть read paths, но не снизили существенно аллокации памяти в протестированной конфигурации.

Для performance-critical .NET-сервисов, особенно под высокой нагрузкой, технологию доступа к данным стоит выбирать на основе реальных характеристик workload, а не по привычке или предпочтению.

Если главный constraint - developer productivity, EF Core обычно хороший default.

Если главный constraint - memory pressure, стабильность latency или производительность complex SQL, Dapper заслуживает серьезного внимания.

Самая практичная архитектура часто выглядит не как EF Core versus Dapper.

Это EF Core там, где помогает абстракция, и Dapper там, где важен контроль.