Почему все столбцы этого запроса выбираются быстрее, чем один столбец, который мне нужен?


13

У меня есть запрос, где использование select *не только делает гораздо меньше операций чтения, но также использует значительно меньше процессорного времени, чем использование select c.Foo.

Это запрос:

select top 1000 c.ID
from ATable a
    join BTable b on b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
    join CTable c on c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
where (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff)
    and b.IsVoided = 0
    and c.ComplianceStatus in (3, 5)
    and c.ShipmentStatus in (1, 5, 6)
order by a.LastAnalyzedDate

Это закончилось с 2473 658 логическими чтениями, главным образом в таблице B. Он использовал 26 562 CPU и имел продолжительность 7 965.

Это сгенерированный план запроса:

План от выбора значения одного столбца На PasteThePlan: https://www.brentozar.com/pastetheplan/?id=BJAp2mQIQ

Когда я переключаюсь c.IDна *, запрос завершается с 107 049 логическими чтениями, довольно равномерно распределенными между всеми тремя таблицами. Он использовал 4266 процессоров и имел продолжительность 1,147.

Это сгенерированный план запроса:

План от выбора всех значений На PasteThePlan: https://www.brentozar.com/pastetheplan/?id=SyZYn7QUQ

Я попытался использовать подсказки запроса, предложенные Джо Оббишем, с такими результатами:
select c.IDбез подсказки: https://www.brentozar.com/pastetheplan/?id=SJfBdOELm
select c.ID с подсказкой: https://www.brentozar.com/pastetheplan/ ? id = B1W ___ N87
select * без подсказки: https://www.brentozar.com/pastetheplan/?id=HJ6qddEIm
select * с подсказкой: https://www.brentozar.com/pastetheplan/?id=rJhhudNIQ

Использование OPTION(LOOP JOIN)подсказки с select c.IDрадикально уменьшило количество операций чтения по сравнению с версией без подсказки, но оно все равно примерно в 4 раза превышает количество select *запросов на чтение без каких-либо подсказок. Добавление OPTION(RECOMPILE, HASH JOIN)к select *запросу сделало его работу намного хуже, чем все, что я пробовал.

После обновления статистики по таблицам и их индексов , используя WITH FULLSCAN, то select c.IDзапрос выполняется гораздо быстрее:
select c.IDперед обновлением: https://www.brentozar.com/pastetheplan/?id=SkiYoOEUm
select * перед обновлением: https://www.brentozar.com/ pastetheplan /? id = ryrvodEUX
select c.ID после обновления: https://www.brentozar.com/pastetheplan/?id=B1MRoO487
select * после обновления: https://www.brentozar.com/pastetheplan/?id=Hk7si_V8m

select *по-прежнему превосходит select c.IDпо общей продолжительности и общему количеству чтений ( select *около половины чтений), но использует больше ресурсов ЦП. В целом они гораздо ближе, чем до обновления, однако планы все же различаются.

Такое же поведение наблюдается в 2016 году в режиме совместимости 2014 года и в 2014 году. Чем можно объяснить несоответствие между этими двумя планами? Может ли быть так, что «правильные» индексы не были созданы? Может ли статистика быть немного устаревшей причиной этого?

Я попытался переместить предикаты до ONчасти объединения несколькими способами, но план запроса каждый раз один и тот же.

После перестроения индекса

Я перестроил все индексы трех таблиц, участвующих в запросе. c.IDвсе еще делает большинство чтений (более чем вдвое больше *), но загрузка процессора составляет около половины *версии. c.IDВерсию также попало на данный TempDb сортировкого ATable:
c.ID: https://www.brentozar.com/pastetheplan/?id=HyHIeDO87
* : https://www.brentozar.com/pastetheplan/?id=rJ4deDOIQ

Я также попытался заставить его работать без параллелизма, и это дало мне самый эффективный запрос: https://www.brentozar.com/pastetheplan/?id=SJn9-vuLX

Я заметил количество выполнений операторов ПОСЛЕ большого поиска по индексу, который выполняет упорядочивание, только выполненное 1000 раз в однопоточной версии, но значительно больше в параллельной версии, между 2622 и 4315 выполнениями различных операторов.

Ответы:


4

Это правда, что выбор большего количества столбцов подразумевает, что SQL Server может потребоваться больше работать для получения запрошенных результатов запроса. Если бы оптимизатору запросов удалось составить идеальный план запросов для обоих запросов, было бы разумно ожидать, чтоSELECT *запрос выполняется дольше, чем запрос, который выбирает все столбцы из всех таблиц. Вы наблюдали обратное в вашей паре запросов. При сравнении затрат следует соблюдать осторожность, но общая стоимость медленного запроса составляет 1090,08 единиц оптимизатора, а общая стоимость запроса составляет 6823,11 единиц оптимизатора. В этом случае можно сказать, что оптимизатор плохо выполняет оценку общих затрат на запрос. Он выбрал другой план для вашего запроса SELECT * и ожидал, что этот план будет дороже, но здесь дело не в этом. Такое несоответствие может происходить по многим причинам, и одна из наиболее распространенных причин - это проблемы с оценкой количества элементов. Операторские расходы в значительной степени определяются оценками мощности. Если оценка количества элементов в ключевой точке плана является неточной, тогда общая стоимость плана может не отражать реальность. Это грубое упрощение, но я надеюсь, что это поможет понять, что здесь происходит.

Давайте начнем с обсуждения, почему SELECT *запрос может быть дороже, чем выбор одного столбца. SELECT *Запрос может превратить некоторые покрывающие индексы в noncovering индексов, что может означать , что оптимизатор должен сделать аддитивную работу , чтобы получить все столбцы, необходимые или , возможно , придется читать с большим индексом.SELECT *может также привести к большим промежуточным наборам результатов, которые должны быть обработаны во время выполнения запроса. Вы можете увидеть это в действии, посмотрев приблизительные размеры строк в обоих запросах. В быстром запросе размеры строк варьируются от 664 до 3019 байт. В медленном запросе размеры ваших строк варьируются от 19 до 36 байт. Операторы блокировки, такие как сортировки или хэш-сборки, будут иметь более высокую стоимость для данных с большим размером строки, потому что SQL Server знает, что дороже сортировать большие объемы данных или превращать их в хеш-таблицу.

Глядя на быстрый запрос, оптимизатор оценивает, что ему нужно выполнить 2,4 миллиона запросов на индексирование Database1.Schema1.Object5.Index3. Вот откуда большая часть стоимости плана. Тем не менее, фактический план показывает, что только 1332 поиска индекса были выполнены для этого оператора. Если вы сравните фактические и оценочные строки для внешних частей этих соединений цикла, вы увидите большие различия. Оптимизатор считает, что для поиска первых 1000 строк, необходимых для результатов запроса, потребуется гораздо больше запросов на индексирование. Вот почему запрос имеет относительно высокую стоимость плана, но он заканчивается так быстро: оператор, который был предсказан как самый дорогой, выполнил менее 0,1% своей ожидаемой работы.

Глядя на медленный запрос, вы получаете план с в основном хеш-соединениями (я считаю, что объединение циклов предназначено только для работы с локальной переменной). Оценки кардинальности определенно не идеальны, но единственная реальная проблема оценки - в конце с сортировкой. Я подозреваю, что большую часть времени тратится на сканирование таблиц с сотнями миллионов строк.

Может оказаться полезным добавить подсказки к обеим версиям запроса, чтобы вызвать план запроса, связанный с другой версией. Полезные советы могут быть полезны для выяснения того, почему оптимизатор сделал свой выбор. Если вы добавите OPTION (RECOMPILE, HASH JOIN)в SELECT *запрос, я ожидаю, что вы увидите план запроса, аналогичный запросу хеш-соединения. Я также ожидаю, что стоимость запроса будет намного выше для плана хеш-соединения, потому что ваши размеры строк намного больше. Это может быть причиной того, что запрос хеш-соединения не был выбран для SELECT *запроса. Если вы добавите OPTION (LOOP JOIN)в запрос, который выбирает только один столбец, я ожидаю, что вы увидите план запроса, аналогичный плану дляSELECT *запрос. В этом случае уменьшение размера строки не должно сильно влиять на общую стоимость запроса. Вы можете пропустить поиск ключевых слов, но это небольшой процент от предполагаемой стоимости.

Таким образом, я ожидаю, что больший размер строки, необходимый для удовлетворения SELECT *запроса, подтолкнет оптимизатор к плану соединения цикла вместо плана хеш-соединения. План соединения цикла стоит дороже, чем должно быть из-за проблем с оценкой количества элементов. Уменьшение размеров строк путем выбора только одного столбца значительно снижает стоимость плана объединения хэшей, но, вероятно, не окажет большого влияния на стоимость плана объединения циклов, поэтому вы получите менее эффективный план объединения хэшей. Трудно сказать больше, чем это для анонимного плана.


Большое спасибо за ваш обширный и информативный ответ. Я попытался добавить подсказки, которые вы предложили. Это сделало select c.IDзапрос намного быстрее, но он все еще выполняет некоторую дополнительную работу, которую выполняет select *запрос без подсказок.
Л. Миллер

2

Несовершенная статистика, безусловно, может привести к тому, что оптимизатор выберет плохой метод поиска данных. Вы пытались делать UPDATE STATISTICS ... WITH FULLSCANили делать полный REBUILDиндекс? Попробуйте и посмотрите, поможет ли это.

ОБНОВИТЬ

Согласно обновлению от ОП:

После обновления статистики по таблицам и их индексов , используя WITH FULLSCAN, то select c.IDзапрос выполняется гораздо быстрее

Итак, теперь, если единственное предпринятое действие было UPDATE STATISTICS, то попробуйте выполнить индекс REBUILD(не REORGANIZE), как я видел, это помогло с подсчетом количества строк, где UPDATE STATISTICSи индекс, и индекс REORGANIZEне сделали.


Мне удалось получить все индексы трех задействованных таблиц для перестроения за выходные, и я обновил свой пост, чтобы отразить эти результаты.
Л. Миллер

-1
  1. Можете ли вы включить индексные скрипты?
  2. Вы устранили возможные проблемы с "анализом параметров"? https://www.mssqltips.com/sqlservertip/3257/different-approaches-to-correct-sql-server-parameter-sniffing/
  3. Я обнаружил, что эта техника полезна в некоторых случаях:
    a) переписать каждую таблицу как подзапрос, следуя этим правилам:
    b) SELECT - сначала поставить столбцы соединения
    c) PREDICATES - перейти в соответствующие подзапросы
    d) ORDER BY - перейти в соответствующие подзапросы, сортируйте на JOIN COLUMNS FIRST
    e) Добавьте запрос-обертку для окончательной сортировки и SELECT.

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

Вот что я имею в виду ....

SELECT ... wrapper query
FROM
(
    SELECT ...
    FROM
        (SELECT ClientID, ShipKey, NextAnalysisDate
         FROM ATABLE
         WHERE (a.NextAnalysisDate is null or a.NextAnalysisDate < @dateCutOff) -- Predicates
         ORDER BY OrderKey, ClientID, LastAnalyzedDate  ---- Pre-sort the join columns
        ) as a
        JOIN 
        (SELECT OrderKey, ClientID, OrderID, IsVoided
         FROM BTABLE
         WHERE IsVoided = 0             ---- Include all predicates
         ORDER BY OrderKey, OrderID, IsVoided       ---- Pre-sort the join columns
        ) as b ON b.OrderKey = a.OrderKey and b.ClientId = a.ClientId
        JOIN
        (SELECT OrderID, ShipKey, ComplianceStatus, ShipmentStatus, ID
         FROM CTABLE
         WHERE ComplianceStatus in (3, 5)       ---- Include all predicates
             AND ShipmentStatus in (1, 5, 6)        ---- Include all predicates
         ORDER BY OrderID, ShipKey          ---- Pre-sort the join columns
        ) as c ON c.OrderId = b.OrderId and c.ShipKey = a.ShipKey
) as d
ORDER BY d.LastAnalyzedDate

1
1. Я постараюсь добавить индексные DDL-скрипты к исходному сообщению, что может занять некоторое время, чтобы «почистить» их. 2. Я проверил эту возможность, очистив кэш плана перед запуском и заменив параметр bind фактическим значением. 3. Я пытался это сделать, но ORDER BYэто недопустимо в подзапросе без TOP, FORXML и т. Д. Я пробовал это без ORDER BYпредложений, но это был тот же план.
Л. Миллер
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.