Почему удаление столбца из индекса улучшило производительность?

Я очень озадачен проблемой, которую только что решил. Да, я решил проблему, но я до сих пор не понимаю, почему исправление сработало.

Итак, у меня есть таблица 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:

  1. игнорируя эту часть индекса в первую очередь, если она действительно присутствует как в индексе, так и в запросе? Этот индекс был разработан с учетом этого запроса и кажется идеальным для него. Или нет?

  2. почему, когда он его использовал, он все замедлял на порядки?

P.S. Вот план: https://www.brentozar.com/pastetheplan/?id=HJJXnyFQC

П.П.С. По-видимому, есть деталь, которую я считал неважной, но она такова: количество запрошенных As и B весьма значительно (десятки тысяч и сотни тысяч соответственно), и я передаю их как строку json, которая, в свою очередь, анализируется с помощью OPENJSON (или скорее Entity Framework Core делает это за меня).

Предоставление запроса и планов запроса (с помощью «Вставки плана») действительно поможет нам вам здесь.

Thom A 20.05.2024 15:13

Насколько велики списки IN? В принципе, SQL Server, безусловно, должен иметь возможность преобразовать это в N*M поиск по (A,B), а не сканировать его.

Martin Smith 20.05.2024 17:28

@MartinSmith, ты совершенно прав. N было примерно 90 000, а M 2 200, что дало 2 миллиарда поисков.

Andriy K 20.05.2024 17:31

Добавлен @ThomA Paln.

Andriy K 20.05.2024 17:32

Ах, ну, 2 миллиарда поисков вполне могут оказаться дороже, чем просто выполнение 90 000 поисков по A и остаточному предикату по B для оценки всех строк, возвращаемых для части A. Каждый поиск должен начинаться с корня и перемещаться по глубине индекса.

Martin Smith 20.05.2024 17:33

@MartinSmith именно этого я и ожидал. Однако по какой-то причине SQL-сервер отказался от этого.

Andriy K 20.05.2024 17:38

явно задокументировано, что иметь такой большой список [в IN] — плохая идея, @AndriyK . Используйте параметр типа таблицы (индексированный) в EXISTS, если вам нужно иметь столько

Thom A 20.05.2024 17:43

Честно говоря, я удивлен, что вы не столкнулись с ошибкой 8623 со списком из 90 000 значений в IN.

Thom A 20.05.2024 17:54

@ThomA - План выполнения показывает, что на самом деле они из JSON, а не длинный список

Martin Smith 20.05.2024 17:55

Но SQL Server не будет знать, сколько значений на самом деле содержит JSON, поэтому предполагаемое количество выполнений операторов — мусор.

Martin Smith 20.05.2024 18:00

Тогда это противоречит запросу, который у них есть в вопросе, @MartinSmith. Хотя мне интересно, как будет работать массив JSON по сравнению с (индексированным) параметром типа таблицы.

Thom A 20.05.2024 18:00

С параметром типа таблицы он может получить лучшие оценки кратности, если используется OPTION (RECOMPILE) или с отложенной компиляцией, поэтому не будет просто предполагать 50 для обоих списков.

Martin Smith 20.05.2024 18:03

@MartinSmith, спасибо, мне было интересно, почему оценки такие неправильные. Что объясняет его.

Andriy K 20.05.2024 18:25

@ThomA EF Core легко создает для меня эти списки JSON, и до сих пор все работало нормально. Параметры табличного типа могут обеспечить более высокую производительность, однако с ними не так легко работать со стороны кода, поэтому нам придется оценить, стоит ли выигрыш затраченных средств.

Andriy K 20.05.2024 18:25

@ThomA OPENJSON получает оценку по умолчанию в 100 строк, и значения не считаются уникальными, поэтому он получит дополнительную сортировку/поток или хеш-агрегат.

Charlieface 20.05.2024 18:53

TVP не так уж и сложно использовать в EF Core, см., например, stackoverflow.com/a/76831915/14868997. Если вы покажете свой существующий код, мы сможем показать, как его улучшить. В любом случае, для такого запроса сложно/неэффективно использовать один многостолбцовый индекс, поскольку предикаты A и B являются OR. Лучше всего использовать два отдельных индекса и использовать объединение индексов (в основном, двойной запрос и INTERSECT).

Charlieface 20.05.2024 18:54

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

Andriy K 20.05.2024 19:31

Лично я не вижу никакого преимущества в использовании пересечения индексов перед простым поиском A и выполнением остаточного предиката для B из покрывающего индекса. При пересечении индексов ему по-прежнему приходится выполнять все поиски A, а теперь необходимо объединить результат со всеми поисками B. Это не похоже на то, что объединенные наборы результатов многих поисков на A и многих поисков на B будут упорядочены каким-либо образом, который облегчит их объединение посредством соединения слиянием. Если он собирается выполнить хеш-соединение, он может с таким же успехом сделать это на основе значения B, полученного из индекса покрытия.

Martin Smith 21.05.2024 09:41

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

Andriy K 23.05.2024 14:06
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
19
123
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Давайте немного подумаем над вашим запросом.

На первый взгляд вы говорите, что

  • A находится в (a1, a2, ..., aN) И
  • B находится в (b1, b2, ..., bM)

Давайте подумаем дальше:

  • (A = a1 OR A = a2 OR ... OR A = aN) И
  • (B = b1 OR B = b2 OR ... OR B = bM)

Дальше:

  • (A = a1 И (B = b1 OR B = b2 OR ... OR B = bM)) OR
  • (A = a2 И (B = b1 OR B = b2 OR ... OR B = bM)) OR
  • ...
  • (A = aN И (B = b1 OR B = b2 OR ... OR B = bM)) OR

Следовательно: у нас есть 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.

В общем, решение — внимательно изучить свой индекс, его порядок и ваш запрос.

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

Lajos Arpad 20.05.2024 17:04

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

Andriy K 20.05.2024 17:25

Проблема не имеет ничего общего с фрагментацией. Это просто плохой план реализации. Показанный план выполнения выполняет поиск 206,719,755 в таблице, содержащей только 113,394,000 строк. Это просто очень неэффективно. По его оценкам, он выполнит только 353 таких поиска, поскольку понятия не имеет, с каким JSON ему придется столкнуться, и не проводит никакого анализа этого при составлении плана.

Martin Smith 21.05.2024 13:14
Ответ принят как подходящий

Благодаря комментариям martin-smith стало ясно, что настоящим виновником был OPENJSON, который использовался как средство для передачи большого списка идентификаторов из Entity Framework Core на SQL Server.

Судя по всему, Query Optimizer предполагает всего 50 элементов в JSON, а когда их становится значительно больше, план выполнения начинает иметь мало общего с реальностью.

Соответственно, удаление столбца из индекса лишило возможности его использовать, и оптимизатор запросов был вынужден использовать «более медленный» план выполнения, который на самом деле намного быстрее из-за большого количества элементов в JSON.

Другие вопросы по теме