Я очень озадачен проблемой, которую только что решил. Да, я решил проблему, но я до сих пор не понимаю, почему исправление сработало.
Итак, у меня есть таблица SQL Server. Довольно большой, в масштабе миллиардов строк. Он имеет кластеризованный индекс по двум столбцам A (~ миллионы уникальных значений) и B (~ тысячи уникальных значений). Комбинация AxB не уникальна. Он также имеет регулярный индекс на B.
По сути, это единственный запрос, который должна обслуживать эта таблица, который сводится к выбору нескольких миллионов строк на основе определенных заданных значений A и B:
WHERE
A IN (a1, a2, ... aN)
AND B IN (b1, b2, ... bM)
Он также группирует и агрегирует результаты, но это не имеет отношения к теме.
В любом случае, при определенных условиях производительность этого запроса резко замедляется. Т.е. только на одном сервере несколько из сотен тестов, которые у нас есть, выполняются в течение 15 минут, в то время как другие подобные обычно занимают менее 10 секунд. Анализ плана выполнения показывает, что на самом деле он сканировал индекс кластера как на A, так и на B, и делал это миллиарды раз. «Быстрые» исполнения фактически полностью игнорировали B как в кластерных, так и во вторичных индексах, просто сканируя A.
На основе этой информации я удалил столбец B из кластерного индекса и исправил тесты.
На всех рассматриваемых машинах установлена версия SQL Server 2022 Developer (фактически все они были установлены всего несколько дней назад), статистика обновляется непосредственно перед выполнением тестов, более того, базы данных фактически доступны только для чтения и восстанавливаются из одной резервной копии. Доступная память и процессоры разные.
Отсюда возникает вопрос(ы): почему удаление второго столбца из индекса фактически улучшило производительность запросов? Насколько я понимаю, это должно было только усугубить ситуацию, но, по-видимому, это улучшило ситуацию.
Почему SQL Server:
игнорируя эту часть индекса в первую очередь, если она действительно присутствует как в индексе, так и в запросе? Этот индекс был разработан с учетом этого запроса и кажется идеальным для него. Или нет?
почему, когда он его использовал, он все замедлял на порядки?
P.S. Вот план: https://www.brentozar.com/pastetheplan/?id=HJJXnyFQC
П.П.С. По-видимому, есть деталь, которую я считал неважной, но она такова: количество запрошенных As и B весьма значительно (десятки тысяч и сотни тысяч соответственно), и я передаю их как строку json, которая, в свою очередь, анализируется с помощью OPENJSON (или скорее Entity Framework Core делает это за меня).
Насколько велики списки IN? В принципе, SQL Server, безусловно, должен иметь возможность преобразовать это в N*M поиск по (A,B), а не сканировать его.
@MartinSmith, ты совершенно прав. N было примерно 90 000, а M 2 200, что дало 2 миллиарда поисков.
Добавлен @ThomA Paln.
Ах, ну, 2 миллиарда поисков вполне могут оказаться дороже, чем просто выполнение 90 000 поисков по A и остаточному предикату по B для оценки всех строк, возвращаемых для части A. Каждый поиск должен начинаться с корня и перемещаться по глубине индекса.
@MartinSmith именно этого я и ожидал. Однако по какой-то причине SQL-сервер отказался от этого.
явно задокументировано, что иметь такой большой список [в IN] — плохая идея, @AndriyK . Используйте параметр типа таблицы (индексированный) в EXISTS, если вам нужно иметь столько
Честно говоря, я удивлен, что вы не столкнулись с ошибкой 8623 со списком из 90 000 значений в IN.
@ThomA - План выполнения показывает, что на самом деле они из JSON, а не длинный список
Но SQL Server не будет знать, сколько значений на самом деле содержит JSON, поэтому предполагаемое количество выполнений операторов — мусор.
Тогда это противоречит запросу, который у них есть в вопросе, @MartinSmith. Хотя мне интересно, как будет работать массив JSON по сравнению с (индексированным) параметром типа таблицы.
С параметром типа таблицы он может получить лучшие оценки кратности, если используется OPTION (RECOMPILE) или с отложенной компиляцией, поэтому не будет просто предполагать 50 для обоих списков.
@MartinSmith, спасибо, мне было интересно, почему оценки такие неправильные. Что объясняет его.
@ThomA EF Core легко создает для меня эти списки JSON, и до сих пор все работало нормально. Параметры табличного типа могут обеспечить более высокую производительность, однако с ними не так легко работать со стороны кода, поэтому нам придется оценить, стоит ли выигрыш затраченных средств.
@ThomA OPENJSON получает оценку по умолчанию в 100 строк, и значения не считаются уникальными, поэтому он получит дополнительную сортировку/поток или хеш-агрегат.
TVP не так уж и сложно использовать в EF Core, см., например, stackoverflow.com/a/76831915/14868997. Если вы покажете свой существующий код, мы сможем показать, как его улучшить. В любом случае, для такого запроса сложно/неэффективно использовать один многостолбцовый индекс, поскольку предикаты A и B являются OR. Лучше всего использовать два отдельных индекса и использовать объединение индексов (в основном, двойной запрос и INTERSECT).
@Charlieface, спасибо, по твоей ссылке достаточно информации, с которой я могу работать. Мне нравится идея пересечения индексов, возможно, это правильно.
Лично я не вижу никакого преимущества в использовании пересечения индексов перед простым поиском A и выполнением остаточного предиката для B из покрывающего индекса. При пересечении индексов ему по-прежнему приходится выполнять все поиски A, а теперь необходимо объединить результат со всеми поисками B. Это не похоже на то, что объединенные наборы результатов многих поисков на A и многих поисков на B будут упорядочены каким-либо образом, который облегчит их объединение посредством соединения слиянием. Если он собирается выполнить хеш-соединение, он может с таким же успехом сделать это на основе значения B, полученного из индекса покрытия.
@MartinSmith, ваши комментарии были точны, на самом деле все сводилось к строкам JSON и неспособности анализатора предсказать количество элементов в них. Если вы можете составить быстрый ответ, я с радостью отмечу его как принятый.





Давайте немного подумаем над вашим запросом.
На первый взгляд вы говорите, что
Давайте подумаем дальше:
Дальше:
Следовательно: у нас есть N операторов и для каждого из них есть M подоператоров.
Второй индекс, как вы отметили, не принес особой пользы. И вам интересно, почему? Причина во многом зависит от вашей конкретной ситуации. Частой проблемой индексов является
Фрагментация индекса возникает, когда логический порядок страниц в индексе не соответствует физическому порядку на диске. Это может привести к тому, что SQL Server будет выполнять больше операций чтения с диска и разделения страниц, снижать производительность и увеличивать использование дискового пространства. Вы можете использовать операторы ALTER INDEX REORGANIZE или ALTER INDEX REBUILD или функцию дефрагментации индекса, чтобы уменьшить фрагментацию индекса.
См. https://www.linkedin.com/pulse/top-10-sql-server-database-issues-can-slow-down-your-application-r-/
Возможно, просто было задействовано слишком большое движение диска, что приводило к некоторой потере производительности за оборот, но при этом было слишком много оборотов, как вы описали размер проблемы.
Также возможно, что во входе было слишком много возможных значений B, и они коррелировали с кластерами, которые индекс имеет для данного значения A.
В общем, решение — внимательно изучить свой индекс, его порядок и ваш запрос.
Я видел, что за этот ответ проголосовали против. Если бы я узнал, в чем причина, что нужно улучшить, то с радостью согласился бы.
Извините, это не я минусовал. Я думаю ты прав. Пытаясь воспроизвести эту проблему, я решил, что на самом деле в экземпляре-нарушителе произошли некоторые изменения, а именно добавлен новый пакет данных, отсутствующий в других копиях. Это, должно быть, привело к фрагментации индекса и вызвало наблюдаемое поведение.
Проблема не имеет ничего общего с фрагментацией. Это просто плохой план реализации. Показанный план выполнения выполняет поиск 206,719,755 в таблице, содержащей только 113,394,000 строк. Это просто очень неэффективно. По его оценкам, он выполнит только 353 таких поиска, поскольку понятия не имеет, с каким JSON ему придется столкнуться, и не проводит никакого анализа этого при составлении плана.
Благодаря комментариям martin-smith стало ясно, что настоящим виновником был OPENJSON, который использовался как средство для передачи большого списка идентификаторов из Entity Framework Core на SQL Server.
Судя по всему, Query Optimizer предполагает всего 50 элементов в JSON, а когда их становится значительно больше, план выполнения начинает иметь мало общего с реальностью.
Соответственно, удаление столбца из индекса лишило возможности его использовать, и оптимизатор запросов был вынужден использовать «более медленный» план выполнения, который на самом деле намного быстрее из-за большого количества элементов в JSON.
Предоставление запроса и планов запроса (с помощью «Вставки плана») действительно поможет нам вам здесь.