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

39

У меня довольно простой запрос

SELECT TOP 1 dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Это дает мне ужасную производительность (как никогда не удосужился дождаться его окончания). План запроса выглядит следующим образом:

введите описание изображения здесь

Однако, если я удаляю, TOP 1я получаю план, который выглядит так, и он выполняется через 1-2 секунды:

введите описание изображения здесь

Правильный PK и индексация ниже.

Тот факт, что TOP 1измененный план запроса меня не удивляет, я просто немного удивлен, что он делает его намного хуже.

Примечание. Я прочитал результаты этого поста и понял, что такое Row Goalи т. Д. Мне интересно узнать, как я могу изменить запрос, чтобы он использовал лучший план. В настоящее время я сбрасываю данные во временную таблицу, а затем извлекаю из нее первую строку. Мне интересно, есть ли лучший метод.

Редактировать Для людей, читающих это после факта, здесь есть несколько дополнительных частей информации.

  • Document_Queue - PK / CI является D_ID и имеет ~ 5 тыс. Строк.
  • Correspondence_Journal - PK / CI имеет значение FILE_NUMBER, CORRESPONDENCE_ID и имеет ~ 1,4 млн строк.

Когда я начинал, других индексов не было. Я получил один в Correspondence_Journal (Document_Id, File_Number)

Кеннет Фишер
источник
1
Есть ли у вас ограничение внешнего ключа, которое обеспечивает DOCUMENT_IDсвязь между двумя таблицами (или у каждой записи CORRESPONDENCE_JOURNALесть соответствующая запись DOCUMENT_QUEUE)?
Даниэль Хутмахер

Ответы:

28

Попробуйте форсировать хеш- соединение *

SELECT TOP 1 
       dc.DOCUMENT_ID,
       dc.COPIES,
       dc.REQUESTOR,
       dc.D_ID,
       cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
INNER HASH JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
       AND dc.QUEUE_DATE <= GETDATE()
       AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER

Оптимизатор, вероятно, думал, что цикл с топ-1 будет лучше, и это имеет смысл, но в действительности это не сработало. Здесь только предположение, но, возможно, предполагаемая стоимость этой катушки была отключена - она ​​использует TEMPDB - у вас может быть плохо работающая TEMPDB.


* Будьте осторожны с присоединиться к намекам , потому что они заставляют порядок доступа к таблице плана в соответствии с письменными порядка таблиц в запросе (так же , как если OPTION (FORCE ORDER)бы были указано). Из ссылки на документацию:

BOL экстракт

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

OPTION (HASH JOIN) Запроса намек может быть менее навязчивой в подходящих случаях, так как это не подразумевает FORCE ORDER. Однако он применяется ко всем соединениям в запросе. Другие решения доступны.

папараццо
источник
1
Похоже, правильный ответ и единственной разницей между ним и более простым планом была дополнительная сортировка спереди.
Кеннет Фишер
3
Не уверен, что мне нравится этот ответ. Подсказки присоединения очень агрессивны. Сначала нужно попробовать некоторые простые изменения индексации, например, индекс в столбце даты.
USR
@usr Это простое соединение PK, которое выполняется менее чем за одну секунду. Довольно безопасная ставка здесь.
Папараццо
4
При форсировании хеш-соединения вы выполняете сканирование большой таблицы. Есть лучшие варианты.
Роб Фарли
30

Поскольку вы получаете правильный план с ORDER BY, может быть, вы могли бы просто бросить свой собственный TOPоператор?

SELECT DOCUMENT_ID, COPIES, REQUESTOR, D_ID, FILE_NUMBER
FROM (
    SELECT dc.DOCUMENT_ID,
           dc.COPIES,
           dc.REQUESTOR,
           dc.D_ID,
           cj.FILE_NUMBER,
           ROW_NUMBER() OVER (ORDER BY cj.FILE_NUMBER) AS _rownum
    FROM DOCUMENT_QUEUE dc
    INNER JOIN CORRESPONDENCE_JOURNAL cj
        ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
    WHERE dc.QUEUE_DATE <= GETDATE()
      AND dc.PRINT_LOCATION = 2
) AS sub
WHERE _rownum=1;

На мой взгляд, план запроса для ROW_NUMBER()вышеупомянутого должен быть таким же, как если бы у вас был ORDER BY. План запроса теперь должен иметь сегмент, проект последовательности и, наконец, оператор фильтра, а остальные должны выглядеть так же, как ваш хороший план.

Даниэль Хутмахер
источник
3
На самом деле, хотя он и предоставлял оператор top (и кучу других вещей (проект последовательности, сегмент и сортировка)), он все еще выполнялся за секунду. Я собираюсь дать правильный ответ @frisbee, так как он был первым и проще. Отличный ответ, хотя.
Кеннет Фишер
10
@KennethFisher, ответ фрисби проще, но в том, как кувалда вбивает финишный гвоздь проще, чем стандартный обрамляющий молоток. Это также сопряжено с большим риском, особенно если оставить его на долгое время. Я бы не стал использовать подобные подсказки, кроме как в тестировании или, может быть, в качестве незначительного исключения.
Стив Мангиамели
@SteveMangiameli В данном конкретном случае есть только одно объединение, поэтому ряд проблем исчезает. Я знаю о рисках использования подсказки соединения (или подсказки запроса). Я просто думаю, что в этом случае это оправдано.
Кеннет Фишер
5
@KennethFisher По моему мнению, основной риск подсказок к запросу заключается в том, что по мере роста или изменения ваших данных применяемый план запроса может стать хуже, чем тот, который система нашла бы самостоятельно. Вы уже видели, как небольшая ошибка в плане может серьезно повлиять на производительность. Использование подсказки в производстве означает: «Я знаю, что этот план всегда будет, всегда будет лучшим, потому что я так хорошо понимаю планировщика и то, как мои данные будут вести себя в течение срока действия этого запроса в производстве». Я никогда не был так уверен в запросе.
jpmc26
29

Редактировать: +1 работает в этой ситуации, потому что оказывается, что FILE_NUMBERэто строковая версия целого числа с нулевым дополнением. Лучшим решением здесь для строк является добавление ''(пустая строка), так как добавление значения может повлиять на порядок, или для чисел добавление чего-то, что является константой, но содержит недетерминированную функцию, например sign(rand()+1). Идея «сломать сортировку» все еще актуальна, просто мой метод не был идеальным.

+1

Нет, я не имею в виду, что я согласен ни с чем, я имею в виду это как решение. Если вы измените свой запрос на, ORDER BY cj.FILE_NUMBER + 1то он TOP 1будет вести себя по-другому.

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

Толщина этих стрелок говорит о том, что ваша DOCUMENT_QUEUEтаблица (DQ) намного меньше вашей CORRESPONDENCE_JOURNALтаблицы (CJ). И что лучший план на самом деле будет проверять через строки DQ, пока не будет найдена строка CJ. В самом деле, именно это и сделал бы Query Optimizer (QO), если бы в нем не было этого противного ORDER BY, что хорошо поддерживается индексом покрытия на CJ.

Поэтому, если вы отбросите ORDER BYполностью, я ожидаю, что вы получите план, который включает в себя вложенный цикл, перебирая строки в DQ, пытаясь найти CJ, чтобы убедиться, что строка существует. И с TOP 1этим это прекратилось бы после того, как был потянут один ряд.

Но если вам действительно нужна первая строка по FILE_NUMBERпорядку, то вы могли бы заставить систему игнорировать этот индекс, который (неправильно) кажется очень полезным, выполняя ORDER BY CJ.FILE_NUMBER+1- что, как мы знаем, будет поддерживать тот же порядок, что и раньше, но, что важно, QO не делает. QO будет сосредоточен на получении полного набора, так что оператор Top N Sort может быть удовлетворен. Этот метод должен создать план, который содержит оператор Compute Scalar для определения значения порядка и оператор Top N Sort для получения первой строки. Но справа от них вы должны увидеть хороший Nested Loop, выполняющий множество поисков на CJ. И лучшая производительность, чем пробежка по большой таблице строк, которые ничего не соответствуют в DQ.

Hash Match не обязательно ужасен, но если набор строк, которые вы возвращаете из DQ, намного меньше, чем CJ (как я и ожидал), то Hash Match будет сканировать намного больше CJ чем это нужно.

Примечание: я использовал +1 вместо +0, потому что оптимизатор запросов может распознать, что +0 ничего не меняет. Конечно, то же самое может относиться к +1, если не сейчас, то в какой-то момент в будущем.

Роб Фарли
источник
7

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

Добавление OPTION (QUERYTRACEON 4138)отключает эффект целей строк только для этого запроса, не слишком излишне предписывая окончательный план, и, вероятно, будет самым простым / наиболее прямым способом.

Если добавление этой подсказки дает вам ошибку разрешений (требуется для DBCC TRACEON), вы можете применить ее, используя руководство плана:

Использование QUERYTRACEONв направляющих план по spaghettidba

... или просто используйте хранимую процедуру:

Какие разрешения QUERYTRACEONнужны? по Кендра Литтл

Мартин Смит
источник
3

Более новые версии SQL Server предлагают различные (и, возможно, лучшие) варианты для работы с запросами, которые получают неоптимальную производительность, когда оптимизатор может применять оптимизацию цели строки. SQL Server 2016 с пакетом обновления 1 (SP1) представил тот DISABLE_OPTIMIZER_ROWGOAL USE HINTже эффект, что и флаг трассировки 4138. Если вы не используете эту версию, вы также можете рассмотреть возможность использования OPTIMIZE FORподсказки запроса, чтобы получить план запроса, предназначенный для возврата всех строк вместо 1. Запрос ниже. выдаст те же результаты, что и в вопросе, но он не будет создан с целью получить только 1 строку.

DECLARE @top INT = 1;

SELECT TOP (@top) dc.DOCUMENT_ID,
        dc.COPIES,
        dc.REQUESTOR,
        dc.D_ID,
        cj.FILE_NUMBER
FROM DOCUMENT_QUEUE dc
JOIN CORRESPONDENCE_JOURNAL cj
    ON dc.DOCUMENT_ID = cj.DOCUMENT_ID
WHERE dc.QUEUE_DATE <= GETDATE()
  AND dc.PRINT_LOCATION = 2
ORDER BY cj.FILE_NUMBER
OPTION (OPTIMIZE FOR (@top = 987654321));
Джо Оббиш
источник
2

Поскольку вы делаете TOP(1), я рекомендую сделать ORDER BYдетерминированный для начала. По крайней мере, это обеспечит предсказуемость результатов (всегда полезно для регрессионного тестирования). Похоже, нужно добавить DC.D_IDи CJ.CORRESPONDENCE_IDдля этого.

Просматривая планы запросов, я иногда нахожу поучительным упростить запрос: возможно, заранее выбрать все соответствующие строки постоянного тока во временную таблицу, чтобы избежать проблем с оценкой мощности на QUEUE_DATEи PRINT_LOCATION. Это должно быть быстро, учитывая низкое количество строк. Затем вы можете добавить индексы к этой временной таблице, если это необходимо, без изменения постоянной таблицы.

Саймон Берч
источник