Предположим, вы работаете с копией большой базы данных Stack Overflow и хотите получить сведения обо всех пользователях, например для экспорта данных. SQL Server не распараллеливает такой запрос — даже если использовать подсказку, которая требует построить параллельный план:
SELECT Id FROM dbo.Users;SELECT Id FROM dbo.Users OPTION (USE HINT('ENABLE_PARALLEL_PLAN_PREFERENCE'));
Здесь я выбираю только идентификаторы, поскольку в больших копиях базы данных Stack Overflow таблица Users занимает более 1 ГБ. SQL Server Management Studio требуется очень много времени, чтобы отобразить в сетке результатов данные такого объёма. А демонстрировать медленную работу SSMS — не моя цель. Если вы собираетесь повторять этот пример, убедитесь, что используете актуальную версию SQL Server Management Studio: я не хочу, чтобы во время экспериментов у вас закончилась оперативная память, как это могло происходить в старых версиях SSMS.
Почему же запрос не выполняется параллельно?
Короткий ответ: SQL Server понимает, что ему придётся отправить по сети миллионы строк результата через один поток, ведущий к приложению пользователя — в данном случае к SSMS. Он не умеет передавать клиенту данные сразу по нескольким сетевым потокам. Более того, наши запросы нередко требуют отсортированных данных, например с помощью ORDER BY. В таком случае результаты необходимо выдавать в определённом порядке, а организовать это при многопоточном формировании результата не так-то просто.
Но… как же заставить SQL Server использовать несколько потоков для чтения данных, а затем объединить их в единый набор, отправляемый по сети? Просто чтобы наглядно показать, почему параллельное выполнение такого запроса — плохая идея.
Для этого нужно обмануть SQL Server, заставив его считать, что запрос содержит избирательное условие фильтрации, которое вернёт лишь небольшое количество строк, хотя на самом деле будут возвращены все строки.
Мы воспользуемся тем, что SQL Server не собирает статистику о результатах математических выражений и практически не пытается анализировать, какими могут быть эти результаты:
SELECT Id FROM dbo.Users
WHERE Id - Id = 0;
Мы с вами знаем: если взять любое число и вычесть его из самого себя, получится ноль. Но SQL Server, каким бы умным он ни был, этого не знает и считает, что условие фильтрации весьма избирательно. Посмотрите на план выполнения:
SQL Server предполагает, что вернётся всего ОДНА СТРОКА! Разве не прекрасно? Мне нравятся такие небольшие особые правила, жёстко зашитые в оптимизатор запросов. Очень хотелось бы видеть жёлтый восклицательный знак над каждым оператором запроса, для которого оценка количества строк определяется подобным встроенным правилом. Ведь очевидно, что оценка в одну строку получена не с помощью какой-то сложной эвристики.
Поскольку SQL Server считает, что условие фильтрации отберёт совсем немного строк, он ожидает, что по сети придётся передать лишь небольшую часть данных. В результате в план выполнения добавляется параллелизм, хотя фактически необходимо прочитать все данные.
Сравним фактические планы выполнения и статистику для однопоточного и параллельного вариантов. Вот что показывает SET STATISTICS TIME:
- Параллельный вариант: время процессора = 16297 мс, общее время выполнения = 46267 мс.
- Однопоточный вариант: время процессора = 5703 мс, общее время выполнения = 43103 мс.
Параллельный вариант потребовал примерно втрое больше процессорного времени. Разумеется, неизвестно, какая часть этих затрат связана с вычитанием числа из самого себя. Но SQL Server также пришлось повторно объединять данные, полученные несколькими параллельными потоками, чтобы отправить их через единственный сетевой поток. И это при том, что мы даже не сортируем результат.
Параллельный запрос на самом деле не завершился быстрее: передача всего этого объёма данных по сети и обработка результата в SSMS, которая отображает его в виде таблицы, занимают огромное количество времени.
Однако самое интересное начинается дальше. Посмотрите на фактические планы выполнения и обратите внимание на время, указанное для их операторов:
Параллельный план ВЫГЛЯДИТ быстрее: кажется, что выполнение заняло всего несколько секунд, тогда как однопоточному варианту потребовалось гораздо больше времени. Однако эти значения показывают лишь то, сколько времени SQL Server потратил на собственную работу, — а не то, сколько времени потребовалось, чтобы данные дошли до клиента. Чтобы увидеть время передачи результата клиенту, включите SET STATISTICS TIME ON. Именно так можно получить приведённые выше значения.
Есть и другой способ убедиться, что параллельный план не даёт никаких существенных преимуществ: сравнить статистику ожиданий. Щёлкните правой кнопкой мыши по каждому оператору SELECT, выберите Properties и найдите раздел со статистикой ожиданий.
- Параллельный план: 171 секунда ожиданий CXPACKET и 43 секунды ожиданий ASYNC_NETWORK_IO.
- Однопоточный план: 37 секунд ожиданий ASYNC_NETWORK_IO.
Итак, параллельный план не ускоряет выполнение, расходует больше процессорного времени и создаёт дополнительные накладные расходы на параллелизм. Именно поэтому, если предложение WHERE не содержит действительно избирательного условия, а запрос возвращает большой объём данных, параллельное выполнение не ускоряет работу — оно делает её медленнее.
А если мы не возвращаем строки клиенту?
Что, если мы записываем строки во временную таблицу? Казалось бы, такой вариант должен позволить использовать параллелизм.
SELECT Id INTO #Temp1 FROM dbo.Users WHERE Id - Id = 0;SELECT Id INTO #Temp2 FROM dbo.Users;
Теперь ситуация иная: оба плана выполнения используют параллелизм.
Никакие ухищрения больше не нужны: даже обычный запрос без условий фильтрации выполняется параллельно.
- С хитрым условием WHERE: время процессора = 25391 мс, общее время выполнения = 7834 мс.
- Без условия WHERE: время процессора = 17703 мс, общее время выполнения = 5674 мс.
Этот пример ещё раз показывает: когда SQL Server должен передавать данные через единственный поток, параллелизм не имеет смысла. Но если целевой объект способен одновременно принимать данные из нескольких потоков, ситуация меняется — параллельное выполнение становится возможным.
Поводом для этой статьи послужила статья Луи Тошона на ту же тему. Я был в полном восторге, когда прочитал её, потому что это одна из моих любимых узкоспециализированных тем.
Эти знания никогда не приносили мне практической пользы в работе с заказчиками: SQL Server и без того прекрасно справляется с подобными ситуациями, и вмешиваться в его поведение не стоит. Но экспериментировать с ним и разбираться, почему он работает именно так, очень интересно.
Могу представить ситуацию, в которой эти знания пригодятся: например, когда SQL Server сильно занижает оценку количества строк и выбирает параллельное выполнение по неверным причинам. Однако в реальной работе я пока не сталкивался именно с таким частным случаем.
Когда-нибудь это обязательно случится! Будем надеяться. А пока, возможно, эти знания однажды пригодятся и вам.





Комментариев нет:
Отправить комментарий