Периодически мне попадаются коды, в которых выполняется подсчет числа строк в некоем множестве, а затем на основе полученного результата принимается то или иное решение. Но при этом учитывается лишь одно обстоятельство: равняется ли число строк нулю. Рассмотрим пример на базе AdventureWorks, показанный на экране 1. Также обратите внимание на план запроса на экране 2, возвращаемый для оператора SELECT.
Экран 1. Пример кода |
Экран 2. План запроса подсчета строк |
Нет ничего удивительного в том, что для выявления соответствующих строк система SQL Server использовала кластеризованный индекс, а для их подсчета — агрегат потока. Но если присмотреться к коду внимательно, становится очевидно, что необходимости в пересчитывании строк нет. Здесь мы имеем дело с проверкой на наличие. Я просто не могу спокойно смотреть на процедуру подсчета. Зачем считать, если задача на самом деле состоит лишь в том, чтобы определить, есть ли здесь строки вообще.
Наличие и оптимизатор
Реализованный в SQL Server оптимизатор — хорошо продуманный программный продукт, который неплохо выполняет свою задачу: защищает нас от самих себя.
К примеру, если мне требуется узнать, имеются ли в базе данных AdventureWorks строки Sales.SalesOrderHeader, для ответа на этот вопрос вовсе не нужно подсчитывать число строк в таблице.
Поясню свою мысль. Давайте сосчитаем строки в таблице Sales.SalesorderDetail и определим, превышает ли их число отметку в 10 строк. Запрос выглядит так, как показано на экране 3. А план запроса представлен на экране 4.
Экран 3. Подсчет числа строк с проверкой |
Экран 4. План запроса для подсчета строк с проверкой |
Здесь нет никаких сюрпризов. Примененный план предполагает подсчет числа строк. Отметим, что объект, избранный системой SQL Server для проведения подсчета, это не сама таблица, а индекс к ней (см. экран 5).
Экран 5. Статистика сканирования индекса к таблице |
Это логично, поскольку индекс включает в себя то же число строк, что и таблица, но при его считывании приходится перерабатывать меньший объем данных, нежели при считывании всей таблицы. И потому SQL Server пересчитывает число строк в индексе.
Для сравнения рассмотрим запрос EXISTS (см. экран 6) и его план на экране 7.
Экран 6. Запрос с EXISTS |
Экран 7. План запроса с EXISTS |
Вполне очевидно, что необходимости пересчитывать строки не было. SQL Server принял решение ограничиться поиском каких-либо строк в самом компактном индексе к таблице, какой он только мог обнаружить. И опять все абсолютно логично.
А теперь давайте посмотрим, что произойдет, если мы внесем изменения в запрос, который пересчитывал строки. Пусть он сравнивает результат не с числом 10, а с нулем (см. экран 8). План запроса представлен на экране 9.
Экран 8. Запрос с COUNT и проверкой с 0 |
Экран 9. План запроса с COUNT и проверкой с 0 |
Здесь SQL Server использовал для версии запроса с COUNT тот же план, что был выбран ранее для версии с EXISTS. SQL Server — достаточно «смышленый» программный продукт, чтобы понимать: если мы пересчитываем значения лишь для того, чтобы определить, будет ли результат больше нуля, то мы на самом деле выполняем запрос EXISTS.
Теперь поставим такой вопрос: не лучше ли будет написать вариант запроса с использованием оператора COUNT? Думаю, что да. И не с точки зрения быстродействия, а из соображений удобства чтения. Сравним два запроса, приведенные на экране 10.
Экран 10. Два варианта запроса |
Я полагаю, мысль о том, что мы просто выполняем проверку на наличие, во втором запросе выражена намного яснее. А удобство чтения — соображение важное.
Переписывание запросов
В ранних версиях SQL Server переписывание запросов выполнялось чрезвычайно редко, и я не ставлю это в вину разработчикам. Ведь всякий раз, переписывая подготовленный кем-то запрос, мы рискуем исказить намерение автора. В ранних версиях SQL Server использовалось множество предикатов, от которых можно было полностью отказаться. Сейчас я продемонстрирую вам пример того, как они используются в текущих версиях SQL Server. Рассмотрим версию запроса, приведенную на экране 11 (план этого запроса показан на экране 12).
Экран 11. Пример усложненного запроса с предикатами |
Экран 12. План выполнения усложненного запроса |
Обратите внимание на то, что в плане запроса таблица Sales.SalesOrderHeader не упоминается. И опять на помощь пользователю приходит оптимизатор. Он определил, что ни одна из строк никогда не сможет соответствовать обоим предикатам в одно и то же время:
SalesOrderID < 100 SalesOrderID > 100
SQL Server пришел к заключению, что ему нет необходимости даже рассматривать данную таблицу. Это важная концепция, так как многие запросы действительно нужно переписывать. Рассмотрим необычный предикат, показанный на экране 13.
Экран 13. Пример запроса, который нужно переписать |
Должен вам сказать, что с такими запросами системе SQL Server приходится иметь дело практически ежедневно. Вы можете считать, что никто, находясь в здравом уме и твердой памяти, не станет использовать подобные логические конструкции, однако именно такой бестолковый код выдают генераторы запросов.
Поручить системе SQL Server заняться переписыванием запросов, перед тем как запускать их на выполнение, — это весьма обоснованная идея.
Пределы разумности
Однако постоянно рассчитывать на «бесплатный сыр» не приходится. SQL Server не может затрачивать слишком много времени на размышления о том, как выполнить тот или иной запрос. Иногда системе нужно просто выполнить его. Это мы можем позволить себе роскошь всестороннего рассмотрения проблемы, а SQL Server не в состоянии целый день решать логические загадки и лишь после этого переходить к выполнению запроса.
Оптимизатору известно множество способов защиты человека от самого себя, но он отнюдь не совершенен. Всегда, когда это возможно, пишите код, который облегчает работу оптимизатора. И, что еще более важно, пишите код, который коллегам было бы легко читать и использовать, иначе говоря, код, в котором явно прослеживаются ваши намерения. Если вы просто хотите уточнить, существует ли тот или иной объект, используйте запрос EXISTS.