Pull to refresh

Comments 78

Чего-то странно вообще. У меня сейчас VPS за 519 рублей в месяц. Там болтается пет-проект с PostgreSQL и API (NestJS) - прямо рядом. Сейчас через ab дёрнул один из API-эндпоинтов - 215.23 rps. Запрос в БД тоже есть, и я там даже с оптимизацией не заморачивался вообще никак. У меня там правда не like, а просто выборка по индексу. Цена вопроса - 519 рублей в месяц. У вас как-то всё слишком дорого вышло. Либо я что-то не так понял.

Server Software:        nginx/1.18.0
Server Hostname:        <мой сайт>
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-CHACHA20-POLY1305,2048,256
Server Temp Key:        X25519 253 bits
TLS Server Name:        <мой сайт>

Document Path:          /api/v2/posts/561-kak-pravilno-chistit-kletku-khomiaka
Document Length:        7247 bytes

Concurrency Level:      100
Time taken for tests:   46.463 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      76000000 bytes
HTML transferred:       72470000 bytes
Requests per second:    215.23 [#/sec] (mean)
Time per request:       464.628 [ms] (mean)
Time per request:       4.646 [ms] (mean, across all concurrent requests)
Transfer rate:          1597.38 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      223  259  51.1    252    1454
Processing:    81  198  95.1    160     810
Waiting:       81  198  95.0    160     810
Total:        313  458 106.0    419    1697

Percentage of the requests served within a certain time (ms)
  50%    419
  66%    461
  75%    511
  80%    542
  90%    596
  95%    642
  98%    707
  99%    755
 100%   1697 (longest request)
Чего-то странно вообще

Да, есть такое. Я мог бы даже статью переназвать так :)


Но, думаю тут проблема в самом запросе от EF. Как видите, даже с LIKE мы уже вывозим сотню за 30 долларов. А на индексах, когда мы проверяем полное вхождение, будет еще быстрее.

Если вы говорите об индексах и LIKE, то при чём тут EF? EF же только генерирует запрос и маппит его результаты на объекты, а сам запрос выполняет БД, и индексы участвуют именно в момент выполнения запроса в БД (на её стороне). Тем более, если у вас LIKE по началу значения поля DisplayName, то индекс и так может использоваться, если у вас он есть, и выполняются условия для того, чтобы БД решила его использовать.

даже с LIKE мы уже вывозим сотню за 30 долларов

И опять-таки даже это крайне дорого для такой производительности. Особенно учитывая, что БД у вас на отдельном хосте и с API за ресурсы не борется.

Если вы говорите об индексах и LIKE, то при чём тут EF?

Потому что EF не генерит LIKE код для того c# с которого я начинал.


И опять-таки даже это крайне дорого для такой производительности

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

Да, кстати. Побуду адвокатом дьявола Azure, в эту сумму входит лицензия, апдейты, и метрики всякие полезные. Я бы это тоже учитывал.

А вот этого не знал, кстати

Может быть, у вас пул соединений для БД не используется?

Тогда мы бы не держали 300 RPS на s1. Я думаю, что дело все таки в выделеных мощностях.

ADO.net пулит коннекшны сам

Ничего странного, запрос автора делает full table scan, так как он просит StartsWith(x). Потому БД и стала узким горлышком. 10 vCore не помогли потому, что БД не умеет выполнять запрос на нескольких ядер. Судя по данным, запрос автора все читал с диска. Ваш запрос работает с индексом, который, скорее всего, полностью помещается в памяти.

Наверное я что-то не понимаю, но в плане выполнения указан Clustered Index Scan. Разве это не означает был использован индекс?

Scan -прохождение по липесточкам осенью чтобы найти желтый красивый среди дерева, seek поиск в желтых липесточках красивых.
Сделайте вычисляемую колонку с хранением(PERSISTED) и сделайте по ней индекс назовите StartWith и сравните с этим полем Users.Where(x=>x.StartWithFromBase==key). Ну или прям на вводе данных выделите эту логику.
Раз у вас така операция произошла то почему бы не разделить эти ключи в отдельное поле? Как так получилось что в реляционном виде вам нужна операция над строками? В общем похоже на ошибку БЛ.

p.s. ещё есть жаркие споры по поводу возможности использовать full text index для такого дела, но я не уверен точно на счет результатов для лайка. Слышал разное мнение.

Нет, ошибки тут нет. Приложение тестовое, и я специально не использовал PK, что бы сильнее нагрузить базу.

Clustered Index — по сути определяет то как данные хранятся на диске, и содержит в себе все данные таблицы. С точки зрения выполнения Scan по нему — это почти аналогично Table Scan'у (который на MSSQL будет только в случае таблицы без кластерного индекса).
Странно что SQL не пытается использовать созданный индекс по DisplayName. Зависит от таблицы, объема столбцов и селективности выборки конечно, но как вариант, для полноты эксперимента — можно попробовать указать его в запросе хинтом принудительно и посмотреть план исполнения. Возможно что-то не так с индексом, и он не подходит для этого запроса.

О как, не знал что скан по кластерному индексу так плох. Спасибо за наводку.


По поводу игнорирования индекса я встречал мнение что при Like запросах индексы не используются, правда встречал и обратные утверждения. Надо будет помучать, еще раз спасибо.

В случае Like — индекс не выйдет использовать, если like написан как «like '%a%'». В случае «like 'a%'» — он может быть использован. Это ограничение исходит из того, что индекс — по сути справочник со ссылками на «части» данных. Для строк — он строится начиная с начала строки, грубо говоря:
— index root.
— s[0]<«n» — go to index_1
— s[0] == «n» — go to index_2
— s[0] > «n» — go to index_3
В случае like %a% — неизвестно с чего начинается строка и SQL не может пойти в нужное поддерево индекса, надо обойти все записи в таблице.
В случае like a% — начало известно, по нему можно отсечь те ветки индекса которые точно не подходят, и проверить только оставшиеся.
Сложно сказть точно, не имея перед глазами структуры таблицы, но, судя по картинкам, у вас первичный ключ таблицы не DisplayName, по началу (первым 3 символам) которого вы отбираете записи, а некий Users_ID. Если так, то индексно-последовательная структура хранения таблицы (в порядке первичного ключа) ничем вам в отборе нужных записей помочь не может, и Clustered Index Scan — он только называется так красиво, а по сути в данном случае он полностью аналогичен Full Table Scan.
Для оптимизации ваших запросов (на выборку) можно попробовать добавить индекс по DisplayName — благо обновлений этой таблицы у вас, как я понял, нет, а потому и нет накладных расходов на ведение этого индекса (но в реальной жизни они иногда существенны).
При создании БД можно было бы указать EF на необходимость создания этого индекса, добавив атрибут [Index] к свойству DisplayName вашего класса данных (он называется Users, если таблица в БД создавалась с имене по умолчанию).
Поможет ли это добавление при уже созданной базе без выполнения миграции — это я сказать не возьмусь.
PS Посмотреть в плане запроса, используется ли кластерный индекс именно как индекс, можно по свойству Ordered его элемента в плане (в статье по ссылке в комментарии от andreyverbin выше про это хорошо показано)
.

первичный ключ таблицы не DisplayName, по началу (первым 3 символам) которого вы отбираете записи, а некий Users_ID

Первичный ключ в таблице просто Id, на DisplayName создан индекс,

Это подтверждается с помощью:

SELECT * 
FROM sys.indexes 
WHERE object_id = OBJECT_ID('DBO.Users')

Который показывает наличие двух индексов.

Что касается создания БД - она создавалась из бекапа и только потом создавался контекст.

Ну да, не видя струтуры базы, ошибиться в предположениях мне было несложно. Тем более, что вы EF используете как database-first, где ytdtljvfz vyt структура БД определяет все.
А почему этот имеющийся индекс по DisplayName не используется в запросах (по плану выполнения это видно) — это я действительно не понимаю.
Впрочем, у планировщика запросов СУБД всегда могут быть свои резоны. Например (но ваш ли это случай — я это утверждать не готов), он не может оценить селективность индекса по начальной подстроке, по которой идет выборка, и действует из наихудшего предположения, что она крайне низкая — а потому последовательный просмотр таблицы лучше и планировщик выбирает его. А не будучи DBA, я даже не знаю, как с этим бороться (и можно ли вобще) в вашей конкретной СУБД.

Я разобрался с планами благодаря вашей подсказке. У меня локально поднято несколько БД, одна из них без индекса, как вы и предполагали. Скриношты планов были сделаны именно нее. А наличие индекса я проверял на облачной БД, там он действительно есть. Сейчас я сделал скриншоты планов из БД в Azure, там совсем другая картина.


Приношу свои извинения за путаницу.

запрос автора делает full table scan, так как он просит StartsWith(x)

А где тут связь? При использовании StartsWith разве не может быть задействован индекс? Это ж по сути LIKE 'blabla%'. Там ещё вопрос правда в количестве записей. Если их мало, БД может и не использовать индекс, посчитав, что фулскан таблицы будет оптимальнее. По крайней мере, в PostgreSQL так, насчёт MSSQL не знаю.

StartsWith превратился в какое-то выражение, которое надо вычислить для каждого пялка таблицы. Поэтому индекс тут не поможет.

если сделать Like 'blabla%' то поможет

У автора, судя по всему, кластерный индекс — не по тому полю, по которому происходит выборка: кластерный индекс всегда хранит данные, упорядоченные по первичному ключу (это еще называется индексно-последовательная схема хранения)
UFO landed and left these words here

но чего не сделаешь ради красивых цифр

А после изменения программы проводили повторное тестирование на ноутбуке?

Хороший вопрос — но нет, не тестировал. Если будет время вечером запущу тесты и добавлю в статью.

UFO landed and left these words here

До AWS в ближайшее время руки точно не дотянуться. А вот async-и я тестировал на варианте с EF:


 private Task<User[]> GetUsersEFAsync(string key)
        {
            return _ctx.Users
                .Where(x => x.DisplayName.Contains(key))
                .OrderBy(x => x.DisplayName)
                .Take(25)
                .ToArrayAsync();
        }

Но разницы не увидел. Возможно это связано с тем что мы быстро исчерпывали вычислительную мощность БД и наличие свободных потоков ничего не давало, т.к. им все равно никто не мог ответить.


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


А по StackOverflow — у них и кеш есть, и 4 SQL сервера. Я на их фоне как бедный родственник :)

UFO landed and left these words here
11000 queries/s с 4% CPU utilization — это про один сервер который как раз StackOverflow обслуживает.

11k это пик, так-то ~6.1k


Кэш не для для SQL
А зачем им кеш? Мне правда интересно.

async нужен в основном чтобы ваш тестовый скрипт не стал бутылочным горлышком

Возможно это причина почему я не смог загрузить на 100% дорогую базу. В случае когда мы видим полное исчерпание DTU — значит увеличение кол-ва запросов к ней ничего не даст и async нам тут бонусов не даст.

UFO landed and left these words here
UFO landed and left these words here

MSSQL и EF это отдельный, уникальный и дивный мир! Я в вебе с тех самых времен, когда верстали еще под IE6, и навыки верстака оценивались знаниями как выравнить блочный элемент по центру, как сделать прозрачность или как не словить баг с margin.

Прошло 15 лет и ничего не поменялось. Я по прежнему занимаюсь шаманством. Переписываю запрос LINQ c `.where(n => n.PropABool && !n.PropBBool)` на `.where(n => n.PropABool == true && n.PropBBool == false)`

Запрос вроде вашего `.Where(x => x.DisplayName.StartsWith(key))` это вообще не позволительная роскош! Нет, тут только полнотекстовый поиск, там все норм со скоростью работы, а обычный like использовать на больших полях опасно!

Вот еще такой пример: `.Where(n => !n.Items.Any())` или `.Where(n => n.Items.Count() == 0)`. Вопрос к знатокам, какой будет работать шустрее?

Я бы предположил, что n.Items.Any() будет быстрее. По логике там не требуется весь "список" просчитывать, достаточно наличия первого совпадения. Так ведь?

Надо смотреть, что там за запрос генерируется в обоих случаях, иначе это гадание на кофейной гуще. Если !n.Items.Any() превращается в NOT EXISTS, то это будет самый оптимальный вариант.

В моем случае .Where(n => n.Items.Count() == 0)работало шустрее в разы. Да и в дальнейшем я понял что лучше в LINQ Any вообще не использовать и делать все на Count. К сожалению пока нет возможности сделать ToString обоих запросов.

Отрицание с Any - это мина замедленного действия. На подобном запросе разваливался MySQL, Postgre в зависимости от фазы луны, оптимизатор MSSQL обычно догадывается до LEFT JOIN, но видно не всегда.

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

Ну и иногда стоит играться с First/Single - в зависимости от ORM и базы данных могут весьма разные результаты получаться.

Я бы еще предложил проверить, как повлияет на производительность изменение выборки на асинхронную: await <...>.ToArrayAsync() вместо блокирующего .ToArray().

Синхронный вызов к БД блокирует поток, что теоретически может привести к исчерпанию потоков в пуле, и так же к таймаутам входящих HTTP-запросов.

Локально на EF ничего не дало. Впрочем вечером сделаю еще два теста локально, посмотрим.

В конце статьи под спойлером еще статистика.

Если кратко то:

  • На SQL разогнались почти до 75 RPS

  • Асинк на РПС не влияет. Видимо все таки упираемся в базу, а не количество запросов.

Часть про локальное тестирование была отредактирована из-за допущеной во время локальных тестов ошибки.

Свежие тесты показали что локально:

  • На EF мы держим 150RPS

  • EF Async - 160 RPS

  • На SQL Sync - 2098 при нагрузке в 2300

  • На SQL Async - 2273.2 при нагрузке в 2400

На планах исполнения теперь отчетливо видно использования Index scan для EF и Index seek для SQL что и объясняет разницу.

Прошу прощения за путаницу, больше времени уделялось перепроверки облака, а вот с локальным тестированием получилась оплошность хотя для сравнения это не менее важно.

Можете на основе своего опыта придумать формулу приведения этих двух попугаев в rps? Хоть я и видел их калькулятор, для рядового разработчика не хватает чакры понять. Чтобы примерно прикинуть остальные тарифы. Хотя бы очень приблизительно

Нет, это очень большой риск всех ввести в заблуждение. Максимум что можно - написать N самых популярных сценариев и провести тестирование на них. Но:

  • Что такое "популярные" сценарии не сильно понятно

  • Потом они обновят железо и/или конфигурации и все, тесты будут только обманывать

Но вашу боль я прекрасно понимаю - как узнать наперед бюджет который нужен для заданого RPS да еще и без миграции в облако (пусть даже и в конкретном случае и пусть даже приблизительно) - ХЗ.

Попробуйте сделать ApiController с асинхронным методом и вызывать ArrayToAsync + AsNoTracking.

Кстати, для Like есть EF.Functions.Like()

.ToArrayAsync() не принес особой пользы, детали под последним катом.

А что касается использования подсказки так я же об этом прямо в статье писал)

По моим ощущениям, аренда железа в облаке стоит столько же в месяц, сколько стоит эквивалентный потребительский комп на рынке. Тут тоже примерно совпало.

Намучались с переносом .Net приложения с ORM NHibernate c дата центра в облако - ORM генерит массу запросов и если в облаке веб сервер и бд на разных машинах (а всегда на разных) то latency предсказать невозможно.

Облака замечательны, но для своих пет-проектов за $120/мес разместил в датацентре 2 стареньких сервера на 40 ядер, 1.5тб памяти и 10тб SSD диска, 15 статических IP и давно отбил начальное вложение в б/у серверы. Все железо под моим контролем и производительность адекватна нагрузкам.

UFO landed and left these words here

Обычные домашние проекты, которые неспеша выходят на коммерческие.

https://www.ebay.com/itm/353200398868 x2 и старые SAS SSD с большим остаточным ресурсом.

Как-то хостил бота для скайпа в azure с использованием botframework. На пробном аккаунте ничего не делающий бот съел 7 тысяч рублей за месяц. С тех пор очень осторожно отношусь к облакам для личных проектов, vps выглядит куда как экономичнее (но больше настроек руками, конечно)

У меня была аналогичная ситуация. С тех пор использую только VPS с фиксированной ценой.

А свою железку, в качестве сервера, так ни не попробовали? и зря. за 7 тысяч рублей можно 8 ядерный ноут купить с рук, SSD прикупить к нему, и радоваться.

Так а смысл? Технической поддержки никакой, всё делаешь сам, в том числе и настраиваешь бэкапы (их же бэкапить надо на другой комп всё равно). Также этот ноут-сервер должен быть постоянно включён и подключён к домашнему интернету (от него целиком и полностью зависит). Кроме того, нужно у провайдера всё равно платить за внешний IP адрес.

Ботам может быть не нужен белый IP, если они работают через лонг пулинг. И хостить что-то на своём NAS вполне себе идея, если не нужен 99.99999% аптайм, конечно (или интернет упадёт или электричества не станет).

Бэкап только на другой комп? А что с облаками? А что со вторым диском? Или внешним диском? Бесплатный скрипт и все автоматически. Так вот по моему опыту ноут без электричества не оставался на долго за пару-тройку лет. В нашем плотненько населенном микрорайоне это исключено, а вот vps арендованный, был недоступен двое суток, что и привело меня к закрытию вопроса аренды.

Ip стоит 80 рублей в месяц, ну что об этом говорить...

На счёт тех поддержки - вы один раз запустите у себя все необходимые компоненты, и будете быстрее любой поддержки решать проблемы, которых и нет, если не лезть с собственными идеями, периодически возникающими.

К счастью это были не реальные деньги, а пробный месячный кредит, да и бот мне особо был не нужен, просто пробовал технологию. А так конечно, для реального бота just 4 fun разумнее завести VPS за 100-200р в месяц.

Если честно такая дичь... не буду дублировать что люди ранее писали. Хочется ускорить запрос... избавляемся от любых преобразований на индексном поле... добавляем покрывающий индекс + вычиляемое поле... помечаем что оно имеет бинарный коллейшен.

Краткий пример о чем речь:

USE AdventureWorks2014
GO

SELECT AddressLine1
FROM Person.[Address]
WHERE AddressLine1 LIKE '%100%'

------------------------------------------------------------------

USE [master]
GO

IF DB_ID('test') IS NOT NULL BEGIN
    ALTER DATABASE test SET SINGLE_USER WITH ROLLBACK IMMEDIATE
    DROP DATABASE test
END
GO

CREATE DATABASE test COLLATE Latin1_General_100_CS_AS
GO
ALTER DATABASE test MODIFY FILE (NAME = N'test', SIZE = 64MB)
GO
ALTER DATABASE test MODIFY FILE (NAME = N'test_log', SIZE = 64MB)
GO

USE test
GO

CREATE TABLE t (
     ansi VARCHAR(100) NOT NULL
   , unicod NVARCHAR(100) NOT NULL
)
GO

;WITH
    E1(N) AS (
        SELECT * FROM (
            VALUES
                (1),(1),(1),(1),(1),
                (1),(1),(1),(1),(1)
        ) t(N)
    ),
    E2(N) AS (SELECT 1 FROM E1 a, E1 b),
    E4(N) AS (SELECT 1 FROM E2 a, E2 b),
    E8(N) AS (SELECT 1 FROM E4 a, E4 b)
INSERT INTO t
SELECT v, v
FROM (
    SELECT TOP(50000) v = REPLACE(CAST(NEWID() AS VARCHAR(36)) + CAST(NEWID() AS VARCHAR(36)), '-', '')
    FROM E8
) t
GO

------------------------------------------------------------------

ALTER TABLE t
    ADD ansi_bin AS UPPER(ansi) COLLATE Latin1_General_100_BIN2

ALTER TABLE t
    ADD unicod_bin AS UPPER(unicod) COLLATE Latin1_General_100_BIN2

CREATE NONCLUSTERED INDEX ansi ON t (ansi)
CREATE NONCLUSTERED INDEX unicod ON t (unicod)

CREATE NONCLUSTERED INDEX ansi_bin ON t (ansi_bin)
CREATE NONCLUSTERED INDEX unicod_bin ON t (unicod_bin)
GO

------------------------------------------------------------------

SET STATISTICS TIME, IO ON

SELECT COUNT_BIG(*)
FROM t
WHERE ansi LIKE '%AB%'

SELECT COUNT_BIG(*)
FROM t
WHERE unicod LIKE '%AB%'

SELECT COUNT_BIG(*)
FROM t
WHERE ansi_bin LIKE '%AB%' --COLLATE Latin1_General_100_BIN2

SELECT COUNT_BIG(*)
FROM t
WHERE unicod_bin LIKE '%AB%' --COLLATE Latin1_General_100_BIN2

SET STATISTICS TIME, IO OFF

Наверное я не очень внятно объяснил цель статьи. Задача не стоит выжать максимум любой ценой. Задача стоит оценить какую нагрузку держит облако в +- типичном сценарии с использованием типичных инструментов. И, заодно, посмотреть как изменятся результаты если поменять запрос, который создает ORM, на не сложный SQL. Все остальное - совсем за рамками этой статьи.

За пример спасибо, за дичь обидно

Задача стоит оценить какую нагрузку держит облако в +- типичном сценарии

я бы не сказал, что сканирование большой таблицы — это типичный сценарий, скорее это то, чего на проде стараются избегать.

Да я согласен :)

Но с меня как с .NET программиста взятки гладки - код работает, я даже индекс накатил. А то, что вместо index seek получился index scan мы узнаем в лучшем случае из мониторинга, а в худшем от заказчиков, т.к. в план выполнения все кто не DBA смотрят, скажем так, не часто. Поэтому для меня этот код вполне подходит как типичный.

Ну и DBA не всегда есть в команде, особенно если ты вообще один.

я бы не сказал, что сканирование большой таблицы — это типичный сценарий, скорее это то, чего на проде стараются избегать.

Все правильно вы пишете, но это, похоже, не тот случай, про который в статье: по крайней мере, в запросе, сгенеренном EF, у него Index Seek (который, подозреваю, ещё идет не с начала индекса, а сразу с нужного места и сразу выбирает запрошенные TOP N записей как раз в запрошенном в ORDER BY порядке то есть явлется практически оптимальным — но для проверки стоило бы посмотреть свойства этого оператора плана)+KeyLookup (потому что у него SELECT * ..., т.е. он забирает и те поля, которых нет в индексе).
В первоначальном выложенном варианте — да, было полное сканирование таблицы, потому что автор тогда выложил планы запросов по таблице без индекса по полю, по начальной части которого у него идет отбор.
PS Я не вижу, к сжалению, код шаблона для страницы, но если там выводятся только имена пользователей (поле DisplayName), то от KeyLookup, скорее всего, можно избавиться даже в сгенернном EF запросе, добавив в цепочку вызовов функций LINQ .Select(x=>x.DislplayName), чтобы получить в запросе SELECT [DisplayName]… (ну, и код шаблона для страницы подправить, чтобы учесть, что в него передаются только строки со значениями DisplayName).
По поводу PS я, возможно, погорячился: не исключено, что там потребуется больше переделок, чтобы избавиться от SELECT * и KeyLookup, как его следствия.

Пару - тройку лет назад, в комментарии под статьей на тему " выбора vps" сервера, я написал, что по моему личному опыту, для mvc, нет ничего лучше собственного ноутбука, а в случае роста нагрузки - собственного сервера с оптикой в дом. После этого заявления я узнал о себе столько нового, нелицеприятного.

Вы ещё забыли упомянуть самую главную боль - негарантированность даже вот этого убогого результата.

У хостинга под личным ноутбуком очень много нюансов. И если для петпроектов это может быть да, то для чего-то существенного скорее нет (или надо понимать конкретный случай).

Вы ещё забыли упомянуть самую главную боль - негарантированность

Ну какой-то SLA там есть, но результаты чуток прыгают, это да.

Честно говоря я готов сказать спасибо вам огромное, за тех, кто решает проблему первичного размещения своего стартапа, например, поскольку вопрос раскрыт, и кто-то, как я, в свое время, не будет искать ответ, и не попадет в зависимость от убогой аренды, и не будет перебирать арендаторов, и страны их места расположения, и нервничать в 100500 раз, когда сервер недоступен или перегружен. Я прошел через этот дурдом, и сожалею о потраченном времени и нервах.

ноут бук это мини сервер с ИБП, которого хватает на полчаса работы при отключенной электросети. это просто подарок судьбы, за смешные деньги, как тут не порадоваться. если ваш портал не несет большую нагрузку, такую как в вашем примере - 100 соединений в секунду, но, жизненно важно держать постоянное соединение, то нет ничего лучше ноутбука.

@Drag13, не понял почему локально "плохой" код EF, так хорошо отрабатывал по сравнению с тем же плохим кодом на Azure.

Потому что ресурсы которые выделяет облако на выбраном тарифе меньше, чем у меня на ноутбуке. Хотите что бы код работал быстрее - или платите больше, или оптимизируйте код.

Обновленные планы выполнения запросов

Похоже, вы вместо «Прямого запроса» повторно выложили план для варианта, сгенеренного EF.
Проверьте, пожалуйста.

Так и есть, выложил правильный. Спасибо.

Вот так лучше: теперь видно, где сгенеренный EF запрос проигрывает, и примерно понятно, почему: видимо, на скорости сканирования индекса сказывается сложность выражения в WHERE, вычисляемого на основе значения индексированного поля.
PS Но для полноценного сравнения было бы лучше, если бы число возвращаемых записей совпадало, а то сейчас у вас сгенеренный EF запрос возвращает 1 запись, а сделанный вручную (с другими параметрами, видимо) — 5

Это очень, очень хороший пример того как внешне "простая" операция оказывается непростой внутри. Самое смешное, ваше ускорение добавило как минимум баг, а фактически уязвимость. Выражение для like надо эскейпить, иначе юзер может заслать вам строку которая начнется с %, например, и отключит индекс или придумает еще варианты для DDOS. В том числе и по этой причине EF Core и генерирует такой странный код. Можно почитать тут https://github.com/dotnet/efcore/issues/474. Расплатой становится не sargable WHERE, а значит в лучшем случае Index Scan вместо Seek, так как надо вычислять LEFT/LEN для каждого ряда. Нагрузка на IO растет в разы, растет и CPU и идет перерасход DTU.

Самое смешное, ваше ускорение добавило как минимум баг, а фактически уязвимость. 

SQL инъекции это не смешно, так что спасибо что обратили на это внимание.
Но в данном случае уязвимости нет, т.к. используется SQL параметры:

var p1 = new SqlParameter("@DisplayName", $"{key}%");
var query = _ctx.Users.FromSqlRaw(
    $"SELECT TOP 25 * FROM USERS WITH (NOLOCK) WHERE DisplayName LIKE @DisplayName ORDER BY DisplayName", p1
    );

Документация подтверждает безопасность данного подхода:

Вы также можете создать DbParameter и указать его в качестве значения параметра. Поскольку используется обычный заполнитель параметров SQL, а не строковый заполнитель, можно безопасно использовать FromSqlRaw.

Если строка будет начинаться с % то перестанет использоваться Index Seek и сервер ваш снова не сможет отдавать 100RPS, отказ в обслуживании, деградация. Попутно можно использовать другие фичи like чтобы еще сильнее нагрузить базу.

Ну да, но это же тест, а не разбор полетов продакшн базы. Если по сильно большому счету, то все равно какой код брать, главное что бы можно было сравнить с облако и локальную машину. А так можно и %test% и test% и даже test, но, наверное, последнее было бы не столь показательно.

Only those users with full accounts are able to leave comments. Log in, please.