Одна запись из пустой таблицы.
Как известно, процесс выполнения запроса состоит из парсинга (проверка синтаксиса), нормализации (привязка метаданных, раскрытие представлений, уплощение подзапросов и пр.), компиляции (процедурных конструкций типа if, циклов) и оптимизации (собственно, SQL-запросов, которые, по определению, являются непроцедурными, т.е. в них описывается лишь то, что хочется получить, но не как). При построении процедурного плана выполнения SQL-запроса, в расчет, в частности, принимаются статистики распределения величин в колонках. Как известно, оптимизатор априoри оценивает мощность результирующего множества в одну запись в случае табличных переменных:
set showplan_all on
go
declare @t table (id int identity)
insert @t default values; insert @t default values; insert @t default values;
select * from @t
go
set showplan_all off
Рис.1
табличных функций (не inline) и др., когда статистики нет, потому что ее изначально не ведется, и сказать что-либо определенное про то, сколько там на выходе будет записей, нельзя.
Но, черт возьми, Холмс (с). Почему из нормальной пустой таблицы он тоже собирается возвращать одну запись?
if OBJECT_ID('dbo.t', 'U') is not null drop table t
create table t (id int)
set statistics profile on
set statistics io on
select id from t
set statistics io off
set statistics profile off
Рис.2
Хотя если посмотреть на вывод statistics io на закладке Messages , там значится
Table 't'. Scan count 1, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Оптимизатор «додумается», что записей нет, если, скажем, в условии where поставить заведомо ложное условие. Меняем запрос и смотрим Estimated Plan.
select id from t where 1 = 0
Рис.3
Имеем 0 записей. Видите – может, когда захочет.
Отсутствие статистики над таблицей t
exec sp_helpstats 'dbo.t'
------------------------
This object does not have any statistics.
не оказывает влияния, т.к. план запроса в данном случае тривиален и не требует статистики распределения величин в колонках. Можно ее создать принудительно, все равно она будет пустой:
create statistics t_stats on dbo.t(id) with fullscan
dbcc show_statistics ('dbo.t', 't_stats')
Рис.4
Можно еще превратить таблицу из кучи в дерево, что, впрочем, тоже не поколеблет решимости оптимизатора получить запись из пустоты:
create clustered index idx_t on dbo.t(id)
select id from dbo.t
Рис.5
Вы скажете, в конце концов, велика ли разница? Ошибка в одну запись мало, на что способна повлиять. Допустим, если оптимизатор ожидает получить n записей, он будет применять индексный поиск, а если n+1, то скан. Когда ситуация балансирует на грани n - n+1, индексный поиск оказывается столь же неоптимален, как и полный скан таблицы.
Разница в одну запись очень велика, когда она оказывается разницей между нулем или не нулем. В частности, если результат запроса планируется пустым, то и выражения в списке SELECT оцениваться не будут, т.к. зачем, если все равно выводить нечего? Например, вот этот запрос отработает нормально:
select 1/0 from t where 1 = 0
Рис.6
А этот вернет ошибку:
select 1/0 from t
Рис.7
То есть даже если мы твердо знаем, что наш запрос не должен возвратить ни одной записи, выражения в списке вывода следует проверять на деление на 0 и прочие каверзы, потому что они могут подвергаться оценке в процессе его выполнения. Это противоречит опыту программирования на алгоритмических языках, которые позволяют выразить шаг за шагом необходимые для выполнения инструкции:
Рис.8
Хотя справедливости ради стоит отметить, что компилятор до определенных пределов может вносить свои коррективы. Например, композитные условия в операторе if в некоторых языках могут оцениваться не в том порядке, как они были прописаны, поэтому там не стоит полагаться на то, что если Cond1 в конструкции if (Cond1 and Cond2) ложно, то до Cond2 дело не дойдет. Тем более это касается непроцедурного языка SQL, в котором запрос позволяет описать лишь то, что нужно делать, а как конкретно это будет делаться, определяется исполнительным механизмом СУБД.
И в завершение. Я не уверен, что дело здесь только в Estimated Row Number, потому что если заменить константу в SELECT на поле таблицы, план запроса не изменится по сравнению с Рис.7, но он не вызовет ошибки
select id/0 from t
select 1/0 from t
Рис.9