MS SQL Server: изучаем планы запросов

20.06.23

База данных - HighLoad оптимизация

Многие знают, что для ускорения работы запроса нужно «изучить план». При этом сам план обычно обескураживает: куча разноцветных иконок и стрелочек; ничего не понятно, но очень интересно! Аналитик производительности Александр Денисов на конференции Infostart Event 2021 Moscow Premiere рассказал, как выполняется план запроса и что нужно сделать, чтобы с его помощью находить проблемы производительности.

Когда заходит речь про оптимизацию тяжелых запросов, разработчик рано или поздно приходит к мысли: «Хорошо, у меня есть план запроса. Это – красивая картинка с кучей каких-то непонятных символов, объектов. И что с этим делать дальше?».

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

 

Как выполняется план

 

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

 

Обычный план на MS SQL выглядит примерно так, как на слайде. Это большое дерево, на котором много объектов. Это дерево - инструкция, что нужно сделать, чтобы получить результат запроса. Операторы в плане вызываются по порядку слева направо (с левого верхнего угла), а данные в плане перемещаются справа налево. Сейчас подробно разберём, как это работает.

 

 

 

Каждый блок на слайде – это оператор, который что-то умеет.

У каждого оператора есть несколько методов, но самый главный из них – Next(), который возвращает одну строку.

Следующий оператор его спрашивает: «Дай строку» и тот отдает. Если надо еще, то снова: «Дай строку» и т.д. И так по одной строке любой оператор выдает вот эти строки.

 

Например, Index Scan отдал строку оператору Sort.

Потом Nested Loops спрашивает у оператора Sort: «Дай строку» – тот ему строку отдал.

Потом уже у Nested Loops спрашивают: «Дай строку». Тот говорит, что не может, так как у него в этом плане есть еще одно поддерево, нужно его сначала выполнить, соединить результаты выполнения. Только после этого он сможет отдать строку.

 

И в этом поддереве повторяется то же самое: по цепочке с правого угла эти строки выдаются.

И после того, как мы получили все строки в Nested Loops, передаем из дальше в Top и выполняем.

 

Важно понимать, что план выполняется справа налево, и именно так движутся данные в вашем плане. Зная это правило выполнения, вы понимаете, куда смотреть.

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

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

 

Живой план выполнения

 

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

К счастью, начиная с 16-й версии MS SQL (и нужна еще Management Studio 18) появилась такая штука, как живой план выполнения. Вы можете поместить план запроса в Management Studio, нажать «Получить живой план выполнения», запустить выполнение запроса, и план просто оживает.

 

 

В этом представлении (в живом плане выполнения) видно, как считается статистика: собирается время выполнения, меняется количество строк, которое выбирается из плана по каждому оператору. Можно посмотреть, как идут данные и в каком порядке.

 

 

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

 

 

Обратите внимание: есть поток данных от Index Scan до сортировки. И дальше данные никуда пока не идут, потому что в данном случае сортировка – это блокирующий оператор. До сортировки движение есть, а дальше – никакого движения нет.

Это то, что вы можете наглядно в живом плане увидеть.

Почему так происходит? Потому что сортировке сначала нужно получить весь объем данных, который она собирается сортировать, и пока она его не прочитает и не отсортирует – она не может продолжать выдавать данные дальше.

Все ждут, когда сортировка получит эти данные.

 

 

И только после этого весь остальной план оживает.

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

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

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

 

Виды планов

 

 

Давайте разберемся, какие планы бывают в MS SQL, и чем они друг от друга отличаются.

Первый вид плана самый простой и самый доступный – Estimated Plan. Это то, что вы можете получить, не выполняя запроса. Даже более того: этот план создается перед выполнением запроса.

По-английски он звучит, как «предполагаемый», «предположительный» план. Но более правильный перевод тут будет - “предварительный”. На самом деле, это тот же самый план с тем же порядком операторов, который будет выполняться дальше. Единственное, чего в нем нет – это статистики о времени выполнения.

Estimated Plan знает, что мы сначала прочитаем из индекса, потом сделаем соединение. Он предполагает по статистике, сколько мы прочитаем из индекса, но точной информации в нем пока нет. Но это тот же самый план с тем же порядком операторов, который будет выполняться дальше.

Live Plan – это то, что вы видели на моих слайдах ранее. Это тот же Estimated Plan, который ожил. В нем мы видим живую статистику выполнения в реальный момент времени. Его удобнее всего наблюдать в Management Studio, хотя есть специальные вьюхи, из которых вы тоже можете вытащить эту информацию и изучать ее сторонними средствами.

Actual Plan – это самая полезная вещь, самая бесценная, но и самая дорогая. Мы не можем себе позволить получать Actual Plan для каждого запроса (это слишком медленно и очень сильно просаживает вашу производительность). Но именно в Actual plan у вас будет статистика по времени выполнения, вы будете знать реальные цифры: сколько времени прошло, сколько и чего было прочитано и т.д.

 

Если мы посмотрим на Estimated Plan, у нас будет просто порядок операций и внизу будут оценки, на которые обычно очень любят ориентироваться.

Не рассчитывайте на корректность этих оценок ни в коем случае! Если у вас более-менее сложный план (от десятка операторов), то скорее всего сумма этих процентов будет больше 100%. Во-первых, это само по себе смешно, а во-вторых, все эти оценки предварительные, они делаются на основе статистик. Если у вас статистики плохие, то и оценки тоже будут плохие, поэтому на них смотреть бессмысленно.

 

В Live Plan предыдущие оценки пропали, но появились другие (на слайде подсвечены желтым).

Первая строчка – это время выполнения на текущий момент. Что важно: обратите внимание, что под каждым оператором сейчас время выполнения одинаковое. В Live Plan у вас счетчик времени выполнения для каждого оператора считается синхронно.

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

  • Например, у оператора Sort сверху мы видим недооценку – 26 из 36. По статистике ожидали прочитать 36 строк, но пока прочитали 26. Либо это ошибка оптимизатора, либо мы просто пока не успели прочитать все, что хотели. Это же Live Plan, он продолжает выполняться. Возможно, сейчас время пройдет, и он дочитает все, что ему было нужно.

  • У оператора Nested Loops внизу та же самая недооценка: 142 из 1614, разрыв еще сильнее.

  • А у других операторов внизу мы видим 80 из 80 – сколько ожидалось, столько и прочитал.

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

 

Ну и Actual Plan – это то, что мы можем увидеть на плане без какого-либо детального изучения.

  • Здесь также отображается статистика, которая была в Estimated Plan. Продолжаем ее игнорировать, потому что смысла в ней никакого нет.

  • Время выполнения: то время, которое в Live Plan тикало для каждого оператора одинаково и синхронно, здесь превратилось в накопительное время. Соответственно каждый оператор знает сколько времени у него потратилось.

  • И осталась статистика по эффективности, по тому, сколько мы ожидали и сколько мы прочитали. Это очень важно именно в контексте Actual Plan, потому что Actual Plan завершился. Мы больше ничего не ждем: все, что могло быть прочитано – прочитано. Поэтому смотрим и изучаем.

 

Что можно увидеть в плане

 

Расскажу, какую дополнительную информацию можно увидеть в планах.

 

Во-первых, как я уже ранее говорил – в Actual Plan можно увидеть накопительное время выполнения.

 

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

Как на слайде: у нас есть Table Spool, на который ушло полчаса, предыдущая операция была выполнена за доли секунды, стало быть, все полчаса у нас ушли на формирование Table Spool.

 

Для более сложного оператора соединения Nested Loops механизм тот же самый: мы просто вычитаем из времени ее выполнения время выполнения всех операторов в дереве, которые стоят перед этим оператором. По слайду мы понимаем, что 46 минут потом отдельно ушло на операцию соединения.

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

 

Еще из интересного у нас есть условия поиска в индексе.

По каждому оператору доступа данных (Seek, Scan – что бы это ни было) мы в секции Seek Predicates можем увидеть, по каким условиям выполнялся поиск.

Это тоже может быть интересно. Например, бывает так, что поиск разваливается на два оператора – сначала мы ищем по некластерному индексу, потом по кластерному и т.д.

Когда понятно, что именно мы достаем из этого индекса, мы можем понять, как это улучшить.

Если мы откроем план в виде .xml (по правой кнопке в Management Studio – Show xml), в полученном тексте мы тоже увидим много интересных вещей.

Во-первых, увидим информацию о статистиках, которые использовались при подготовке этого плана. Вы можете посмотреть, какие статистики были и, самое главное, в каком они были состоянии. Для этого не нужно ждать выполнения запроса и получать Actual Plan: эта информация доступна в Estimated Plan.

ModificationCount – это сколько изменений в статистике накопилось с момента пересчета. Если это значение зашкаливает, то пересчитываем статистики и, теоретически, план должен нормализироваться. И наоборот: если здесь все хорошо, то скорее всего что-то не так с запросом, и мы его как-то плохо написали.

 

Еще одна крутая штука, которая появилась в MS SQL Server 2016 – это статистика по ожиданиям. Она доступна в Actual Plan.

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

Все эти типы ожиданий – стандартные системные, они очень легко гуглятся. На сайте Microsoft приведен полный список всех типов ожиданий, там все это подробно объяснено.

Если вы поймали Actual Plan по какому-то тяжелому запросу, статистика по ожиданиям тоже может многое вам объяснить.

И последний пример, который я привожу (но не последнее, что можно там найти) – это метаинформация о компиляции плана.

В ней можно посмотреть, сколько было памяти, какая степень параллелизма, почему было принято решение параллелить, не параллелить и пр.

Вверху очень интересное сообщение, почему закончилась оптимизация плана: из-за TimeOut.

Если вы такое видите, скорее всего, у вас слишком сложный план, очень большое дерево перебора вариантов и SQL-сервер просто не успевает перебрать все предоставленные варианты. В какой-то момент он останавливается, говорит: «Все! Хватит! Timeout! Берем то, что есть!» Это обычный знак того, что вы работаете не самым лучшим планом, могло бы быть и лучше, если бы вы упростили свой запрос.

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

 

Как искать проблемы в плане

 

Ну и самое интересное: как искать проблемы в плане?

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

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

Это очень простая, но в то же время эффективная техника: берем и сравниваем, сколько мы ожидали и сколько мы прочитали.

Если у вас такой же разрыв, что ожидали 10 строк, а прочитали 4,5 миллиона – первая и наиболее вероятная причина, что у вас просто протухла статистика. Из-за неправильной статистики MS SQL Server неправильно оценил число строк в вашей операции и построил неправильный план.

Обратите внимание, как изначально выглядит этот план – у нас Scan, потом какая-то сортировка, а потом – Nested Loops.

 

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

Самое важное – теперь вместо Nested Loops у нас Merge Join. Это совершенно другой оператор.

Nested Loops очень хорошо работает, когда у вас мало строк на входе (как ожидалось 10 строк), а когда MS SQL понял, что у вас будет много строк, он сразу отменил Nested Loops и поставил вместо него Merge Join.

Если сравнить:

  • в первом исходном варианте у оператора Top время выполнения 19 минут;

  • когда мы пересчитали нашу статистику, время выполнения стало 9 минут.

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

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

 

Второй пример: ищем перепады в числе строк.

Для этого у меня есть запрос, который прикидывается запросом списка.

У нас есть справочник «Контрагенты», мы первые 25 строк выбираем, упорядочиваем по полю ОКВЭД (допустим, бухгалтеру так захотелось). Ожидаем, что у нас все будет достаточно быстро: всего-то надо забрать какие-то 25 строчек из таблицы.

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

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

Здесь та ситуация, когда у нас возник порог – на вход на сортировку приходит много данных (широкая стрелка), а после сортировки очень мало.

Если вспомнить запрос, становится понятно, в чем дело – у нас просто нет индекса для сортировки по полю ОКВЭД. Соответственно, чтобы достать первые 25 строк, приходится прочитать вообще всю таблицу и отсортировать ее. Если таблица большая, то, возможно, что-то даже в tempdb выгрузится. И только после того как сортировка будет готова, мы вытаскиваем вот эти первые 25 строк.

 

Лечение очень простое: если мы понимаем, что по нашему полю упорядочивания нет индекса, просто добавляем индекс в конфигураторе.

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

И последний пример: проверяем «попадание» условий в наши индексы.

Здесь запрос немного интереснее: мы соединяем контрагентов самих с собой, отбираем их по КПП, по наименованию группы и соединяем по родителю.

В целом ничего криминального в этом нет – обычный запрос, может немного перекрученный.

 

И план здесь тоже достаточно обычный – все сбалансировано, каких-то перепадов по стрелкам у нас нет. Все более-менее хорошо.

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

Здесь у оператора Index Seek условия делятся на две группы: Seek Predicate и Predicate. И то и другое – это условия отбора, и то и другое – фильтруется, но в разных ситуациях.

  • Seek Predicate – это то, что мы фильтруем и проверяем прямо на этапе чтения индекса с жесткого диска или из памяти.

  • Predicate – это то, что мы потом дофильтровываем. Если кто-то помнит, когда интернет был маленьким, у «Яндекс» была кнопка «Искать в найденном». Естественно, это гораздо хуже и медленнее, чем искать сразу то, что тебе нужно. И вот Predicate – это то самое «Искать в найденном». Если вы видите Predicate, вы понимаете, что условие у вас работает по вашему индексу, но оно работает недостаточно эффективно. Лучше бы он сразу искал в Seek Predicate в тот момент, когда он читает эти данные.

Разбираемся дальше, почему так происходит.

 

Смотрим состав индекса, из которого мы читаем. И видим вот такую картинку:

  • Зеленые – это те поля, которые мы вытаскиваем в рамках нашего запроса.

  • Желтое – это то поле, которого в запросе нет.

Все, что левее желтого поля Folder, попало в Seek Predicate. И СУБД шла по ключу индекса, искала по полям, по каким могла, а потом споткнулась о Folder, потому что Folder в условиях запроса не было, она не знает, какой Folder должен быть.

MS SQL споткнулся об этот Folder и все, что после него, ушло дальше в условия Predicate.

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

Решение тоже достаточно простое – мы просто берем и добавляем дополнительное условие «ЭтоГруппа = ЛОЖЬ», чтобы полностью попадать в индекс.

 

После этого условия оператора Index Seek преображаются – то, что было в Predicate, уходит в Seek Predicate. Мы максимально эффективно ищем наши данные, и все счастливы.

Это важно, потому что таким образом мы:

  • во-первых, экономим чтение и не засоряем память лишними страницами, которые мы бы отсекли на этапе Predicate;

  • во-вторых, мы экономим время, потому что происходит оптимизация, запрос работает быстрее.

 

Что почитать по теме

 

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

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

  • Во-первых, фундаментальная статья Remus Rusanu о том, как вообще работает SQL Server и как выполняются запросы. Статья очень большая, в ней есть всего понемногу, но качественно и по делу. Я настолько восхитился, что подготовил перевод этой статьи.

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

  • И последняя ссылка – это инструмент Microsoft, который показывает типичные ошибки в плане запроса. Он встроен в Management Studio начиная с версии 17.4. Похож на SonarQube для планов запросов. Это – статический анализ, там какие-то очень простые примеры. Но, чтобы набить руку и примерно представлять, как это работает и на что обращать внимание, можно посмотреть и этот источник. Сложные вещи он не выловит, но хотя бы какую-то базу вам даст.

  • И еще одна вещь, которую я забыл указать на слайде: есть очень старая книжка Дена Тоу «Настройка SQL. Для профессионалов». В бумажном виде вы ее никогда и нигде уже не найдете, потому что она была издана в 2004 году, и ее уже раскупили. Но в электронном виде где-то можно найти. Книжка старая, но до сих пор актуальная. Она описывает фундаментальный низовой уровень оптимизации запросов. Там примеры для четырех СУБД. Есть ощущение, что в фирме «1С» читали эту книжку, потому что там примеры как раз на SQL Server, PostgreSQL, Oracle и DB2 – все, что в 1С поддерживается. Если вы эту книжку достанете, прочитайте обязательно – она очень хорошо поправляет голову в плане оптимизации.

 

*************

Статья написана по итогам доклада (видео), прочитанного на конференции Infostart Event 2021 Moscow Premiere.

См. также

SALE! 15%

Инструментарий разработчика Роли и права Запросы СКД Программист Платформа 1С v8.3 Управляемые формы Запросы Система компоновки данных Конфигурации 1cv8 Платные (руб)

Набор инструментов программиста и специалиста 1С для всех конфигураций на управляемых формах. В состав входят инструменты: Консоль запросов, Консоль СКД, Консоль кода, Редактор объекта, Анализ прав доступа, Метаданные, Поиск ссылок, Сравнение объектов, Все функции, Подписки на события и др. Редактор запросов и кода с раскраской и контекстной подсказкой. Доработанный конструктор запросов тонкого клиента. Продукт хорошо оптимизирован и обладает самым широким функционалом среди всех инструментов, представленных на рынке.

10000 руб.

02.09.2020    159668    875    399    

862

Запросы Программист Платформа 1С v8.3 Запросы Конфигурации 1cv8 Бесплатно (free)

Столкнулся с интересной ситуацией, которую хотел бы разобрать, ввиду её неочевидности. Речь пойдёт про использование функции запроса АВТОНОМЕРЗАПИСИ() и проблемы, которые могут возникнуть.

11.10.2024    5204    XilDen    36    

80

HighLoad оптимизация Инструменты администратора БД Системный администратор Программист Платформа 1С v8.3 Конфигурации 1cv8 Абонемент ($m)

Обработка для простого и удобного анализа настроек, нагрузки и проблем с SQL сервером с упором на использование оного для 1С. Анализ текущих запросов на sql, ожиданий, конвертация запроса в 1С и рекомендации, где может тормозить.

2 стартмани

15.02.2024    12455    241    ZAOSTG    82    

115

HighLoad оптимизация Системный администратор Программист Бесплатно (free)

При переводе типовой конфигурации 1C ERP/УТ/КА на PostgreSQL придется вложить ресурсы в доработку и оптимизацию запросов. Расскажем, на что обратить внимание при потерях производительности и какие инструменты/подходы помогут расследовать проблемы после перехода.

20.11.2023    13561    ivanov660    6    

81

HighLoad оптимизация Запросы

Очень немногие из тех, кто занимается поддержкой MS SQL, работают с хранилищем запросов. А ведь хранилище запросов – это очень удобный, мощный и, главное, бесплатный инструмент, позволяющий быстро найти и локализовать проблему производительности и потребления ресурсов запросами. В статье расскажем о том, как использовать хранилище запросов в MS SQL и какие плюсы и минусы у него есть.

11.10.2023    19358    skovpin_sa    15    

105

Запросы Инструментарий разработчика Программист Бесплатно (free)

Список всех популярных обработок.

17.03.2023    60237    kuzyara    89    

190
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. kser87 2470 20.06.23 12:00 Сейчас в теме
Повторение мать учения.
KirillZ44; METAL; +2 Ответить
2. redfred 20.06.23 12:17 Сейчас в теме
Важно понимать, что план выполняется справа налево, и именно так движутся данные в вашем плане.


План, всё же, выполняется слева направо. Данные движутся справа налево, это правда.

Например, Index Scan отдал строку оператору Sort.
Потом Nested Loops спрашивает у оператора Sort: «Дай строку» – тот ему строку отдал.
Потом уже у Nested Loops спрашивают: «Дай строку».


В данном случае случае nested loop будет вызывать sort через метод open() и потом запрашивать строки через next(), а тот, в свою очередь будет так же вызывать index scan
magic1s; user757190; user865160; +3 Ответить
7. Филин 370 21.06.23 10:15 Сейчас в теме
(2) По порядок выполнения vs. направление потока данных - да, действительно, это может быть важное уточнение. Спасибо, поправлю

Про Open() - пардон, это все-таки обзорная статья (которая выросла вообще из получасового выступления) - поэтому не хотелось перегружать ее такими деталями. Тут самое важное, что по модели выполнения каждый оператор выдаёт по одной строке за раз. Кому надо будет разобраться подробнее - может пройти по первой ссылке из дополнительных материалов, там как раз это хорошо разобрано
3. redfred 20.06.23 13:16 Сейчас в теме
Во-первых, как я уже ранее говорил – в Actual Plan можно увидеть накопительное время выполнения.


Емнип, не всё так просто. Если выполнялось в row mode - да, там сумма времени выполнения узла и его дочернего дерева. А если в batch mode - там выводится время только по узлу и нужно время по всем узлам складывать. А ещё с какой-то версии в рамках одного запроса для разных операторов могут быть использованы разные режимы.
8. Филин 370 21.06.23 10:18 Сейчас в теме
(3) про разные режимы - в первый раз я про это услышал в 2019, но не разу не видел вживую. (Точнее, видел, но там одной из таблиц был объявлен columnstore индекс, что для 1С - крайне нетипичная ситуация). Если правильно помню, batch mode on rowstore должен был появиться в 2022 - но тут я уже перестал следить.
В целом, правильное замечание, но, повторюсь, в статье было важно дать общий обзор
4. PerlAmutor 155 21.06.23 07:08 Сейчас в теме
Решение тоже достаточно простое – мы просто берем и добавляем дополнительное условие «ЭтоГруппа = ЛОЖЬ», чтобы полностью попадать в индекс.

Только это будет уже совсем другой запрос. Я столкнулся с проблемой такой оптимизации, когда надо было ускорить выполнение запроса в котором отбирались ключи аналитики учета номенклатуры без учета Назначения (ERP). В индекс запрос не попадал, но там реально надо было найти все ключи без учета Назначения. Т.е. проблема заключается в том, чтобы попасть в индекс мне бы пришлось сначала выбрать все варианты Назначений куда-то во временную таблицу, а потом связывать условие по ним. Почему SQL не умеет "пропускать" (ЭтоГруппа = ANY) сканируя индекс непонятно. Возможно прокатил бы вариант "ЭтоГруппа В (ИСТИНА, ЛОЖЬ)", но тут мы четко знаем 2 варианта значений, а когда это тысячи вариантов ссылок, то уже все.

В качестве альтернативы для "ANY" попробуйте кто-нибудь в условие запроса поставить такую вот конструкцию:

ГДЕ
	Т.ЭтоГруппа = Т.ЭтоГруппа


Полезет ли SQL сервер в основную таблицу или все-таки будет сравнивать ключи внутри индекса сами с собой. Отпишитесь о результатах )
siamagic; +1 Ответить
5. serferian 27 21.06.23 08:20 Сейчас в теме
(4) с любыми Ссылками тоже просто можно добавить

Ссылка > Значение(Справочник.чточтотам.ПустаяСсылка)
siamagic; +1 Ответить
12. PerlAmutor 155 21.06.23 19:59 Сейчас в теме
(5) Тогда уж ">=", пустые ссылки тоже имеют право на существование в базе. Только это не универсально, т.к. реквизит может быть составного типа или вообще не иметь типа "Справочник.чточтотам".
9. Филин 370 21.06.23 10:24 Сейчас в теме
(4)
это будет уже совсем другой запрос.


Формально - да, практически - нет. Камон, мы сортируем по коду Оквэд, который заведомо определён только для элементов. Поэтому условие только уточнит уже существующие ограничения

(4)
Почему SQL не умеет "пропускать" (ЭтоГруппа = ANY) сканируя индекс непонятно

Потому что тут не будет условия на равенство (это будет условие попадания в группу). Соответственно, отвалится вариант Merge Join (которым, кстати, в примере в итоге всё закончилось). Может быть, получится еще применить Hash match - но тут уже СУБД будет оглядываться на статистику. Не зря где-то в глубинах "методических рекомендаций" 1С советует превращать ИЛИ в соединение двух запросов через ОБЪЕДИНИТЬ ВСЕ - это как раз для того, чтобы можно было получить Merge Join (ну и понятно, что надо проверять это применительно к конкретной ситуации)
Дмитрий74Чел; +1 Ответить
6. user1950534 21.06.23 08:54 Сейчас в теме
Вот вы пишите, план запроса выполняется справа налево. Вангую, что через год (может раньше), у нас на собеседованиях, как это принято, начнут задавать тупой вопрос вроде "как выполняется план запроса, в какую сторону"

А я не считаю, например, что он выполняется справа налево

Потому что оптимизатор начинает выполнение с планирования, с декомпозиции запроса. И разбивает его на блоки слева направо. И это для меня - начало выполнения запроса. Вот.
10. Филин 370 21.06.23 10:29 Сейчас в теме
(6)
на собеседованиях, как это принято, начнут задавать тупой вопрос


Пардон, play stupid games - win stupid prizes.

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

Ну и вообще, по опыту, когда у меня на собесе (с обеих сторон) начинало происходить что-то непонятное, я просто начинал обсуждение. "вы спрашиваете, в какую сторону выполняется запрос - это зависит от того, как мы его представим. Давайте разберем на примере, вот я на салфетке нарисовал простенький план, вот тут будут прочитаны первые строки...." Как правило, через 5-10 минут всё становится понятно. И собеседующим про меня - и мне про собеседующих
Cmapnep; Дмитрий74Чел; user1950534; +3 Ответить
11. van_za 269 21.06.23 15:48 Сейчас в теме
Спасибо, мне полезно.
Филин; +1 Ответить
13. PerlAmutor 155 21.06.23 20:10 Сейчас в теме
В ERP 2.5.8 есть веселенький запрос, который вызывается каждый раз когда пользователь нажимает кнопку "Добавить" в табличной части Товары документа ВнутреннееПотреблениеТоваров (в ERP 2.5.12 его изменили):

ВЫБРАТЬ
	СпрАналитикаУчетаНоменклатуры.Ссылка КАК АналитикаУчетаНоменклатуры,
	&Организация КАК Организация,
	&Подразделение КАК Подразделение,
	&ХозяйственнаяОперация КАК ХозяйственнаяОперация
ПОМЕСТИТЬ ОтборПрочихРасходов
ИЗ
	Справочник.КлючиАналитикиУчетаНоменклатуры КАК СпрАналитикаУчетаНоменклатуры
ГДЕ
	СпрАналитикаУчетаНоменклатуры.Номенклатура = &Номенклатура
	И &Характеристика

ИНДЕКСИРОВАТЬ ПО
	АналитикаУчетаНоменклатуры,
	Организация,
	Подразделение,
	ХозяйственнаяОперация
;

////////////////////////////////////////////////////////////­////////////////////
ВЫБРАТЬ ПЕРВЫЕ 1
	ПрочиеРасходы.СтатьяРасходов КАК СтатьяРасходов,
	ПрочиеРасходы.АналитикаРасходов КАК АналитикаРасходов
ИЗ
	РегистрНакопления.ПрочиеРасходы КАК ПрочиеРасходы
ГДЕ
	ИСТИНА В
			(ВЫБРАТЬ ПЕРВЫЕ 1
				ИСТИНА
			ИЗ
				ОтборПрочихРасходов КАК Отбор
			ГДЕ
				ПрочиеРасходы.АналитикаУчетаНоменклатуры = Отбор.АналитикаУчетаНоменклатуры
				И ПрочиеРасходы.Организация = Отбор.Организация
				И ПрочиеРасходы.Подразделение = Отбор.Подразделение
				И ПрочиеРасходы.ХозяйственнаяОперация = Отбор.ХозяйственнаяОперация)

УПОРЯДОЧИТЬ ПО
	ПрочиеРасходы.Период УБЫВ
Показать


Несмотря на всю его простоту он может выполняться от 10мс до 30 минут на одних и тех же данных. Несмотря на то, что тут и в индексы все попадает. Причина банальная - автоматическая статистика для временных таблиц.
SQL сервер по какой-то причине решил, что во временной таблице сотни тысяч записей (хотя они туда ни при каких обстоятельствах не попали бы), поставил слева РегистрНакопления.ПрочиеРасходы, сделал по нему полный скан, а для временной таблицы ОтборПрочихРасходов свалился в Table Spool.(Lazy Spool).
Попробуйте тут обновить статистику на временной таблице...
15. Филин 370 21.06.23 23:16 Сейчас в теме
(13)
Попробуйте тут обновить статистику на временной таблице...

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

Потому что цель статьи совершенно в другом. Это не сборник рецептов "как нам писать запросы". Это введение в диагностику проблем с запросами - то есть, в методологию, как при помощи плана найти проблемное место. Вот вы в план посмотрели, увидели Table Spool - и теперь знаете, какое место лечить. А методов лечения, как правило, больше, чем один и разбирать их - совсем другая история.
16. redfred 22.06.23 12:10 Сейчас в теме
(13)
SQL сервер по какой-то причине решил, что во временной таблице сотни тысяч записей (хотя они туда ни при каких обстоятельствах не попали бы)


Емнип, временные таблицы, по большому счёту, не удаляются, а делают вид, что удаляются. SQL server их очищает и хранит для переиспользования, чтоб каждый раз не создавать структуры с нуля. Причем хранит вместе со статистикой, так что может быть, в первый раз таблица создавалась с большим количеством записей, а переиспользовалась уже с малым, но с первоначальной статистикой
17. Филин 370 22.06.23 12:38 Сейчас в теме
(16)
Временные таблицы кэшируются на двух уровнях. И SQL Server вместо удаления хранит их в кэше, а потом просто возвращает при необходимости, если структура совпадает. И 1С делает практически то же самое - точно так же сохраняет ссылки на временные таблицы и потом переиспользует по возможности.

Теоретически, после truncate статистики должны обновиться (на самом деле, всё немного сложнее https://littlekendra.com/2016/12/08/does-truncate-table-reset-statistics/) - но тут действительно есть, где ошибиться оптимизатору
18. redfred 22.06.23 17:12 Сейчас в теме
(17)
Теоретически, после truncate статистики должны обновиться (на самом деле, всё немного сложнее https://littlekendra.com/2016/12/08/does-truncate-table-reset-statistics/) - но тут действительно есть, где ошибиться оптимизатору


Кендра про обычные таблицы пишет, временные немного по другому работают. Вплоть до того, что можно случайным образом получить статистику из временной таблицы созданной раньше в соседней сессии.
Филин; +1 Ответить
19. PerlAmutor 155 23.06.23 06:24 Сейчас в теме
(18)

Ссылка не открывается из-за лишнего символа в конце

https://littlekendra.com/2016/12/08/does-truncate-table-reset-statistics/
sulfur17; Филин; +2 Ответить
21. siamagic 26.06.23 10:11 Сейчас в теме
(13)
ПрочиеРасходы.АналитикаУчетаНоменклатуры = Отбор.АналитикаУчетаНоменклатуры


"ПрочиеРасходы.АналитикаУчетаНоменклатуры = Отбор.АналитикаУчетаНоменклатуры" -

выглядит как говнокод ради экономии строчек - пробуйте внутренние соединение
22. PerlAmutor 155 26.06.23 19:40 Сейчас в теме
(21) Прочтите мое сообщение еще раз. Это код из типовой. Внутреннее соединение не решит тут ровным счетом ничего, дело не в индексах и не в стиле кода.
14. PerlAmutor 155 21.06.23 20:15 Сейчас в теме
Вот еще один, страдает той же болезнью, что и запрос выше. Вызывается при проведении Этапа на производство 2.2:

ВЫБРАТЬ РАЗЛИЧНЫЕ
	Таблица.Распоряжение КАК Распоряжение,
	Таблица.Номенклатура КАК Номенклатура,
	Таблица.Характеристика КАК Характеристика,
	Таблица.Назначение КАК Назначение
ПОМЕСТИТЬ ВтОтборПоПродукции
ИЗ
	РегистрНакопления.ОбеспечениеПроизводственныхПроцессов КАК Таблица
ГДЕ
	Таблица.Регистратор В(&МассивСсылок)
	И Таблица.Требуется > 0

ИНДЕКСИРОВАТЬ ПО
	Распоряжение,
	Номенклатура,
	Характеристика,
	Назначение
;

////////////////////////////////////////////////////////////­////////////////////
ВЫБРАТЬ
	Таблица.Распоряжение КАК Ссылка
ПОМЕСТИТЬ ВтНезавершенныеЗаказы
ИЗ
	(ВЫБРАТЬ
		Таблица.Распоряжение КАК Распоряжение,
		Таблица.Номенклатура КАК Номенклатура,
		Таблица.Характеристика КАК Характеристика,
		Таблица.Назначение КАК Назначение
	ИЗ
		РегистрНакопления.ОбеспечениеПроизводственныхПроцессов КАК Таблица
			ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВтОтборПоПродукции КАК Отбор
			ПО (Отбор.Распоряжение = Таблица.Распоряжение)
				И (Отбор.Номенклатура = Таблица.Номенклатура)
				И (Отбор.Характеристика = Таблица.Характеристика)
				И (Отбор.Назначение = Таблица.Назначение)
	
	СГРУППИРОВАТЬ ПО
		Таблица.Распоряжение,
		Таблица.Номенклатура,
		Таблица.Характеристика,
		Таблица.Назначение
	
	ИМЕЮЩИЕ
		СУММА(Таблица.Требуется) * (1 - &ДопустимоеОтклонениеОтгрузкиПриемкиМерныхТоваров / 100) > СУММА(Таблица.Произведено)) КАК Таблица

СГРУППИРОВАТЬ ПО
	Таблица.Распоряжение
Показать


Аналогично может выполняется десятки миллисекунд, либо десятки минут.
37. triviumfan 97 11.07.23 23:32 Сейчас в теме
(14) Запрос писал какой-то рукоблуд.
Хорошо бы ещё вводные данные указать: хотя бы платформу и субд.
ЗЫ: и всё равно я не верю, что он может выполняться десятки минут даже с кривой статистикой. Плана нет - значит пустые слова!
20. PerlAmutor 155 23.06.23 06:57 Сейчас в теме
Вот тоже любопытная статья по поводу кэширования временных таблиц

https://www.sql.kiwi/2012/08/temporary-object-caching-explained.html

И эта тоже

https://www.sql.kiwi/2012/08/temporary-tables-in-stored-procedures.html

Похоже лишь с помощью двух шагов можно избавится от использования устаревшей статистики временных таблиц:

    UPDATE STATISTICS #Temp;

    SEL ECT
        T.[Name],
        OrderCount = COUNT_BIG(DISTINCT TH.ReferenceOrderID)
    FR OM #Temp AS T
    JOIN Production.TransactionHistory AS TH
        ON TH.ProductID = T.ProductID
    GROUP BY
        T.Name
    OPTION (RECOMPILE);
Показать


UPDATE STATISTICS #Temp и OPTION (RECOMPILE);
Филин; +1 Ответить
23. itmind 308 27.06.23 05:36 Сейчас в теме
Статья хорошая, актуальная для 2021 года.
Вот только в 2023 году все больше организаций переходят или планируют переходить на PostgreSQL.
24. siamagic 27.06.23 07:06 Сейчас в теме
(22) Я понял что из типовой и мое сообщение про разработчика типовой, я тоже работаю на ерп - какая нагрузка? Как сэмулировать эту ситуацию?

" какой-то причине решил, что во временной таблице сотни тысяч записей" - если так то может первые 100 - поможет?

Или у регистра указать условия у виртаульной таблицы.
25. PerlAmutor 155 27.06.23 18:41 Сейчас в теме
(24) Это никак не поможет, т.к. временная таблица с точно такой же структурой могла использоваться в совсем другом запросе, даже в другой базе 1С на том же SQL сервере, где действительно выбиралось большое количество записей. А что решит первые 100, когда нужно выбрать 30000 записей например? Или предлагаете сделать запросы в цикле, выбирать как-то по 100, а потом результаты соединять? Но это тоже не поможет, т.к. ПЕРВЫЕ (TOP) выполняется над результатом соединения в большинстве случаев, т.е. в последнюю очередь, когда например отсортировал уже 10 миллионов записей.
26. siamagic 28.06.23 07:44 Сейчас в теме
(25) Сам сказал что там записей заведомо мало это раз.

Разберем,

В первом запросе берутся все сочетания аналитик с заданной организацией, допустим есть две организации, у первой нет этой аналитики у второй их 10ть, выберется 10 записей для первой - нафига?

У прочих расходов у виртуальной таблицы можно задать организацию и подразделение, в условии указываешь хозоперацию и аналитику.
27. PerlAmutor 155 28.06.23 18:16 Сейчас в теме
(26) Не в индексах дело и не в ПЕРВЫЕ. Дело в кривом плане запроса, который строится на основании кривой статистики временной таблицы и добавляет операцию Table Spool выделяя память под несуществующий объем данных, хоть у тебя и 1 запись во временной таблице отбора.
Этого не будет происходит только, если не будут использоваться вообще временные таблицы. Например при попытке передать массив или список значений с несколькими сотнями ссылок через оператор "В(&СписокСсылок)", при превышении определенного порога количества в списке (даже одинаковых значений) - платформа автоматически создает якобы новую временную таблицу. Затем SQL сервер находит закэшированную (не используемую) временную таблицу в tempdb с этим же набором полей и тянет её, чтобы не создавать новую. Тянет вместе с кривой статистикой, которая осталась от похожего запроса её создавашего, но совершенно в другом месте и для других целей. А дальше берет из кэша планов запроса - кривой план, если он уже выполнялся. Либо строит новый кривой план основываясь на такой же кривой статистике. Т.е. тут 2 беды - не обновленная статистика временных таблиц и закэшированные кривые планы запросов созданные на основании этой статистики.
28. siamagic 29.06.23 07:51 Сейчас в теме
(27) оооо оказывается любой запрос с временными таблицами работает как попало. Тогда всё понятно с тобой ))

Ну и раз есть такое мнение о ВТ - убрать в этом запросе её вообще не проблема
29. PerlAmutor 155 29.06.23 08:24 Сейчас в теме
(28) Разработчики ERP и убрали, о чем я написал в первом сообщении. Переделали на выборку из РегистрСведений.АналитикаУчетаНоменклатуры
30. Филин 370 29.06.23 11:43 Сейчас в теме
(28)
оказывается любой запрос с временными таблицами работает как попало.


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

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

Ситуация не такая уж частая, но если этот парад планет сошёлся, то тут действительно можно переписать запрос в угоду большей стабильности
31. siamagic 29.06.23 12:03 Сейчас в теме
(30) ой.."некоторые" а у других всё норм - как хорошо лепить чушь на выдуманную проблему.

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

Было бы интересно заэмулировать ситуацию - у меня не вышло добиться таких тормозов.

ссылки на 2012 - .... один мат
32. Филин 370 29.06.23 12:24 Сейчас в теме
(31) Слушайте, это какой-то диалог про проблемы шаманизма на крайнем севере - только наоборот. Вам со ссылками объясняют ситуацию - да, редкую, но все же возможную - а вы встаете в позу "я проблемы не вижу, значит её нет!".
История с кэшем временных таблиц действительно тянет на отдельное исследование. Может быть, в свежих версиях СУБД что-то поменялось со статистиками, но я помню, что видел похожие ситуации. Все это решалось точечными патчами "по месту", потому что в общем случае всё работает как надо. Поэтому у меня рука не поднимается огульно говорить "всё это ерунда и отмазки криворуких программистов".
33. siamagic 29.06.23 13:29 Сейчас в теме
(32)

1. У тебя есть конкретные критерии для этой ситуации, но ты привести пример эмуляции такого поведения не можешь.

2. Под твои критерии попадает почти все запросы 1с - поэтому ты не можешь привести пример эмуляции такого поведения.
34. Филин 370 29.06.23 13:34 Сейчас в теме
(33)
1. Это плавающая ошибка, для которой надо дождаться "парада планет". Такие штуки по определению плохо моделируются, лучший способ найти такое - набрать побольше статистики и найти аномалии. Например, если есть система мониторинга, которая хранит снимки кэша планов - по таким данным эту проблему можно диагностировать. И тут, пардон, я сольюсь - доступа к такой статистике у меня больше нет.
2. Еще раз, для воспроизведения ошибки нужно, чтобы сошлось много факторов. Поэтому я не могу привести надёжное repro
35. siamagic 29.06.23 14:14 Сейчас в теме
(34) Звучит что у человека это было не раз в год и не два = можно. К тому же автор написал рецепт:

Например при попытке передать массив или список значений с несколькими сотнями ссылок через оператор "В(&СписокСсылок)", при превышении определенного порога количества в списке (даже одинаковых значений) - платформа автоматически создает якобы новую временную таблицу. Затем SQL сервер находит закэшированную (не используемую) временную таблицу в tempdb с этим же набором полей и тянет её, чтобы не создавать новую. Тянет вместе с кривой статистикой, которая осталась от похожего запроса её создавашего, но совершенно в другом месте и для других целей. А дальше берет из кэша планов запроса - кривой план, если он уже выполнялся. Либо строит новый кривой план основываясь на такой же кривой статистике. Т.е. тут 2 беды - не обновленная статистика временных таблиц и закэшированные кривые планы запросов созданные на основании этой статистики.
36. siamagic 29.06.23 14:44 Сейчас в теме
Самое очевидно забыл - удалить временную таблицу после запроса или добавление рандомного поля по виду уид
Оставьте свое сообщение