Заметки Дмитрия Пилюгина о Microsoft SQL Server 

Twitter RSS
Home SQL Server (все заметки) Оптимизатор (ч.4): Optimization: Full Optimization: Search 1
formats

Оптимизатор (ч.4): Optimization: Full Optimization: Search 1

Optimization: Full Optimization: Search 1

В данном разделе:
— update statistics with row_count, page_count;
— преобразования memo;
— параллельный план;

 

Данная фаза, называется также Quick Plan. Как мы уже говорили, запросы могут миновать стадию Transaction Processing (search 0), и сразу перейти к этой фазе, если в запросе менее трех таблиц.
Также эта фаза примечательна тем, что на ней осуществляется поиск параллельного плана.

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

Начнем с того, что изменим наши данные так, чтобы план запроса, найденный на предыдущей стадии search0, перестал удовлетворять внутреннему порогу оптимизатора для этой стадии и оптимизатор продолжил поиски на стадии search1.
Для этого, нужно увеличить размер исследуемых таблиц, однако, мы воспользуемся другим приемом, а именно недокументированными опциями команды update statistics — with row_count, page_count. Эти опции изменяют метаданные об индексах, устанавливая те значения, которые мы укажем.

Помните, что все операции мы выполняем на тестовом сервере.

Выполним:

select
	[allocation_units: data_pages] = au.data_pages,
	[partitions: rows] = p.rows,
	[partition_stats: in_row_data_page_count] = ps.in_row_data_page_count,
	[partition_stats: row_count] = ps.row_count
from
	sys.allocation_units au
	join sys.partitions p on au.container_id = p.hobt_id
	join sys.dm_db_partition_stats ps on ps.partition_id = p.partition_id
where
	p.[object_id] in (object_id('t1'),object_id('t2'),object_id('t3'))

update statistics t1 with rowcount = 1000000, pagecount = 1000
update statistics t2 with rowcount = 1000000, pagecount = 1000
update statistics t3 with rowcount = 1000000, pagecount = 1000

select
	[allocation_units: data_pages] = au.data_pages,
	[partitions: rows] = p.rows,
	[partition_stats: in_row_data_page_count] = ps.in_row_data_page_count,
	[partition_stats: row_count] = ps.row_count
from
	sys.allocation_units au
	join sys.partitions p on au.container_id = p.hobt_id
	join sys.dm_db_partition_stats ps on ps.partition_id = p.partition_id
where
	p.[object_id] in (object_id('t1'),object_id('t2'),object_id('t3'))

Результат (до и после):

Вы видите, что метаданные таблиц изменились, теперь, количество страниц и строк в таблицах стало равным тому, что мы указали.

Поговорим немного подробнее об этой команде.
Хотя это опции команды update statistics, но в реальности сама статистика не обновляется.
Посмотрим, к примеру, какие есть статистики для таблицы t1:

select * from sys.stats WHERE object_id = object_id('t1')

У меня это:

Одна статистика для кластерного индекса, и две автоматически созданные статистики для колонок b,c.

Прим. Кстати, как формируется имя автоматической статистики, последняя часть – это object_id объекта для которого создана статистика, дальше идет id столбца, дальше префикс Sys, ну а WA – встречал в литературе такое описание, что якобы это Washington, место, где располагается команда разработки, однако люди, которые сейчас работают в оптимизаторе сказали, что этой истории не помнят. Впрочем, может, помнит кто-то другой, пока будем считать это просто легендой.

Давайте выполним команду для просмотра статистики по первичному ключу (у вас наверняка будет другой идентификатор первичного ключа, так что поменяйте на свой):

dbcc show_statistics(t1,PK__t1__3BD0198E0F624AF8)

Результат:

Как видите, никаких данных о 1000000 строк нет. Т.е. команда update statistics with row_count,page_count – не затрагивает саму статистику.

Более того, если вы выполните (заменив PK__t1__3BD0198E0F624AF8 на то значение, которое будет у вас):

update statistics t1 PK__t1__3BD0198E0F624AF8 with rowcount = 1000000, pagecount = 1000

Команда пройдет успешно, но если вы выполните (т.е. попытаетесь обновить данные по статистике без индекса):

update statistics t1 _WA_Sys_00000002_0D7A0286 with rowcount = 1000000, pagecount = 1000

То результатом будет:
Msg 3739, Level 11, State 3, Line 1
Cannot UPDATE the index ‘_WA_Sys_00000002_0D7A0286’ because it is not a statistics collection.

Если вы попытаетесь вернуть все значения назад, при помощи команды:

update statistics t1 with fullscan
update statistics t2 with fullscan
update statistics t3 with fullscan

select object_name(ps.[object_id]), ps.in_row_data_page_count, ps.row_count
from sys.dm_db_partition_stats ps
where ps.[object_id] in (object_id('t1'),object_id('t2'),object_id('t3'))

То результат останется прежним:

Все это говорит о том, что при обновлении метаданных в данном случае, реальная статистика никак не затрагивается, а обновляются только метаданные индекса. Поэтому, конечно было бы логичнее поместить этот функционал, например, в alter index, но, впрочем, т.к. эта опция недокументированна, то и претензии предъявлять неуместно.

По этому, вернуть значения к «нормальным» можно перестроив индексы:

alter index all on t1 rebuild;
alter index all on t2 rebuild;
alter index all on t3 rebuild;

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

Теперь, выполним наш запрос, с информацией по стадиям оптимизации:

select *
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
option(recompile
,querytraceon 3604
,querytraceon 8675
)

Результат:

End of simplification, time: 0 net: 0 total: 0 net: 0
end exploration, tasks: 89 no total cost time: 0.002 net: 0.002 total: 0.003 net: 0.003
end search(0),  cost: 9.35972 tasks: 207 time: 0 net: 0 total: 0.004 net: 0.004
end exploration, tasks: 460 Cost = 9.35972 time: 0.001 net: 0.001 total: 0.006 net: 0.006
end search(1),  cost: 8.5812 tasks: 895 time: 0.003 net: 0.003 total: 0.01 net: 0.01
end exploration, tasks: 896 Cost = 8.5812 time: 0 net: 0 total: 0.01 net: 0.01
end search(1),  cost: 8.5812 tasks: 1397 time: 0.003 net: 0.003 total: 0.014 net: 0.014
End of post optimization rewrite, time: 0 net: 0 total: 0.014 net: 0.014
End of query plan compilation, time: 0 net: 0 total: 0.014 net: 0.014

Мы видим, что запрос действительно прошел стадии, упрощения, search 0. Почему в выводе search1 встречается два раза? Дело в том, что эта фаза может быть повторно выполнена для поиска параллельного плана. Стоимость 8.5812, больше заданного по умолчанию значения 5.
Чтобы посмотреть, какое пороговое значение стоимости для параллелизма задано у вас, выполните:

name minimum maximum config_value run_value
cost threshold for parallelism 0 32767 5 5

Теперь, измените значение на 10 и перезапустите запрос:

exec sp_configure 'cost threshold for parallelism', 10
reconfigure with override
go
select * 
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
option(recompile
,querytraceon 3604
,querytraceon 8675
)

Вы увидите, что теперь вывод:

Configuration option 'cost threshold for parallelism' changed from 10 to 10. Run the RECONFIGURE statement to install.

End of simplification, time: 0 net: 0 total: 0 net: 0
end exploration, tasks: 89 no total cost time: 0 net: 0 total: 0.001 net: 0.001
end search(0),  cost: 9.23264 tasks: 207 time: 0 net: 0 total: 0.002 net: 0.002
end exploration, tasks: 460 Cost = 9.23264 time: 0 net: 0 total: 0.003 net: 0.003
end search(1),  cost: 8.45349 tasks: 887 time: 0.003 net: 0.003 total: 0.007 net: 0.007
End of post optimization rewrite, time: 0 net: 0 total: 0.007 net: 0.007
End of query plan compilation, time: 0 net: 0 total: 0.007 net: 0.007

Т.е. повторный запуск фазы search1 и исследования для параллельного плана, не производятся, т.к. стоимость 8.45349 меньше значения10, после которого нужно производить поиск параллельного плана.

Не забудем все вернуть на круги своя:

exec sp_configure 'cost threshold for parallelism', 5
reconfigure with override
go
exec sp_configure 'show advanced options', 0
reconfigure with override
go

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

А пока, давайте взглянем на то, как между стадиями оптимизации преобразуется структура memo.
Для этого выполним запрос с некоторыми недокументированными флагами, описанными в предыдущей части:

select * 
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
option(recompile
,querytraceon 3604 --output to console
,querytraceon 8675 --optimization phase info
,querytraceon 8608 --initial memo
,querytraceon 8615 --final memo
)

Я не буду приводить здесь вывод с консоли, т.к. он довольно объемный.
Приведу сокращенный результат в виде рисунка.

Из этого результата видно, что на стадии search 1, в качестве входного, изначального memo используется результат предыдущей стадии search 0 (вы можете сравнить вывод с консоли, я например, сравнивал вывод memo, сохранив результат в два текстовых файла, при помощи Total Commander). Результаты 2 и 3 – полностью совпадают.
После этого, идет исследование альтернатив плана на стадии search 1, и оно отображается в части 4. Для стадии exploration при поиске параллельного плана, новое memo не строится, используется то, что было на стадии поиска последовательного, и в результате, мы получаем итоговое memo — 5. После этого выполняется поиск самого дешевого плана и в данном случае, при одинаковых стоимостях, выбирается последовательный.

Вернемся к вопросу, есть ли способ, заставить оптимизатор использовать параллельный или последовательный план? В случае последовательного плана, работает документированный хинт option(maxdop 1), а как насчет параллельного? Да, возможность есть, но она не документирована, это использование очередного флага трассировки 8649.

Выполним наш запрос и посмотрим на план:

select * 
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
option(recompile
,querytraceon 3604
,querytraceon 8649
)

Мы видим, что теперь, мы получили параллельный план.
Если выполнить этот запрос, со всеми, упомянутыми ранее, включенными флагами трассировки, то мы не найдем в memo физический оператор Parallelism. Однако, в выходном дереве физических операторов, будет присутствовать операция Exchange Start:

Exchange Start
PhyOp_Apply lookup TBL: t2 (0) (x_jtInner)
    PhyOp_Apply lookup TBL: t3 (0) (x_jtInner)
        PhyOp_Filter
            PhyOp_Range TBL: t1(1) ASC  Bmk ( QCOL: [opt].[dbo].[t1].a) IsRow: COL: IsBaseRow1001 
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [opt].[dbo].[t1].b
                ScaOp_Const TI(int,ML=4) XVAR(int,Not Owned,Value=1)
        PhyOp_Range TBL: t3(1) ASC  Bmk ( QCOL: [opt].[dbo].[t3].c) IsRow: COL: IsBaseRow1007 
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier QCOL: [opt].[dbo].[t1].a
                ScaOp_Identifier QCOL: [opt].[dbo].[t3].c
    PhyOp_Range TBL: t2(1) ASC  Bmk ( QCOL: [opt].[dbo].[t2].b) IsRow: COL: IsBaseRow1004 
        ScaOp_Comp x_cmpEq
            ScaOp_Identifier QCOL: [opt].[dbo].[t2].b
            ScaOp_Identifier QCOL: [opt].[dbo].[t3].c

Это говорит о том, что используется оператор параллелизм. В случае, когда в плане отображаются другие операторы параллелизма, например Parallelism — Repartition streams, вы можете увидеть в дереве операцию «Exchange Partition» и т.д. Почему мы не видим отдельных «параллельных» операторов. Дело в том, что параллелизм в сиквеле достигается не за счет распараллеливания отдельных операторов, а за счет распараллеливания всего плана (хотя в нем могут быть и последовательные ветки, на что мы позже посмотрим). Нагляднее это продемонстрирует позаимствованная мной картинка из блога Craig Freedman-а.

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

dbcc optimizer_whatif(1,4)
go
select * 
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
option(recompile
,querytraceon 3604
,querytraceon 8649
)

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

dbcc optimizer_whatif(1,1)
go
select * 
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
option(recompile
,querytraceon 3604
,querytraceon 8649
)

То вы получите последовательный план.

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

Однако, параллельный план возможен не всегда.

Выполним такой запрос, с флагом параллельного запроса:

create function dbo.uf_sf(@a int) 
returns int
as begin return 1; end;
go
select *, dbo.uf_sf(1)
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
option(recompile
,querytraceon 8649)
go
drop function dbo.uf_sf;

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

Далее, другой запрос:

create function dbo.uf_mtvf(@a int) 
returns @t table(a int)
as begin
insert into @t select 1;
return
end
go
select *
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
	cross apply dbo.uf_mtvf(1) 
where
	t1.b = 1
option(recompile
,querytraceon 8649)
go
drop function dbo.uf_mtvf;

План:

Multi-statement Table Valued Function – требуют последовательной ветки в плане, однако не ограничивает в параллелизме весь план.

И, наконец, inline table function:

create function dbo.uf_itvf(@a int) 
returns table
as return select c as a from t3 where c = @a
go
select *
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
	cross apply dbo.uf_itvf(a) 
where
	t1.b = 1
option(recompile
,querytraceon 8649)
go
drop function dbo.uf_itvf;

План:

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

create function dbo.uf_itvf(@a int) 
returns table
as return select c as a from t3 where c = 10
go

Теперь, выполним запрос:

select *
from
	t1
	join t2 on t1.a = t2.b
	join t3 on t2.b = t3.c
where
	t1.b = 1
	and object_id('t1') = object_id('t1')
option(recompile
,querytraceon 8649)

План:

Использование некоторых (но не всех) встроенных функций, также ограничивает параллелизм.

Предложение top может форсировать появление последовательной ветки в параллельном плане.
Выполним запросы и посмотрим на планы:

select *
from
	t1
	join t2 on t1.a = t2.b
	cross apply (select top(1) c from t3 where t3.c = 10 order by c) t3
where
	t1.b = 1
option(recompile
,querytraceon 8649
)
select *
from
	t1
	join t2 on t1.a = t2.b
	cross apply (select top(1) c from t3 where t3.c = t2.b order by c) t3
where
	t1.b = 1
option(recompile
,querytraceon 8649
)

Результат:

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

select *
from
	t1
	join t2 on t1.a = t2.b
	cross apply (select top(1) c from t3 where t3.c = t2.b order by c) t3
where
	t1.b = 1
option(recompile
,querytraceon 8649
)
;
select *
from
	t1
	join t2 on t1.a = t2.b
	cross apply (select top(1) c from t3 where t3.c = t2.b order by c) t3
where
	t1.b = 1
option(recompile)

Посмотрим на план и оценки:

Посмотрим на оператор Clustered Index Scan t1.
Parallel Plan: 1.2932 (CPU = 0.550079)
Serial Plan: 1.84328 (CPU = 1.10016)
В случае параллельного плана, стоимость CPU снизилась в два раза (1.10016/2=0.55008), т.е. сервер масштабировал оценку, учитывая, что просмотр таблицы может происходить в несколько потоков. Что повлияло на общую оценку этого оператора.

Посмотрим на оператор Clustered Index Seek t3.
Parallel Plan: 2.12011 (CPU = 0.0001581)
Serial Plan: 2.12011 (CPU = 0.0001581)
Как мы помним, для операции lookup в Nested Loops в поле CPU оператора отражается всегда стоимость одной операции, поэтому не будем смотреть на то, что значение 0.0001581 и там и там одинаково. Однако, что интересно, и общая стоимость оператора никак не изменилась, не было сделано никакого масштабирования в зависимости от числа процессоров. Знак параллельного выполнения в данном случае, может означать то, что операторы во внутренней части цикла Nested Loops, выполняются последовательными потоками не зависящими от DOP. Об это также можно почитать в статье Paul White-а, и в упомянутом выше блоге Craig Freedman-а.

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

select * 
from
	t1
	join t3 on t1.c = t3.c
where t3.c = 1
option(recompile
,querytraceon 8649
)
select * 
from
	t1
	join t3 on t1.c = t3.c
where t3.c = 1
option(recompile,
maxdop 1
)

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

На этом, мы завершаем разговор про параллельные планы и стадию search 1.

Не забудем вернуть метаданные индексов к нормальному состоянию, перестроив их:

alter index all on t1 rebuild;
alter index all on t2 rebuild;
alter index all on t3 rebuild;

upd.06.09.2012
В 2012 сервере появились новые интересные элементы плана выполнения. В том числе, касающиеся параллельного плана.
Например, можно посмотреть причину, почему не сгенерировался параллельный план.
За это отвечает недокументированный атрибут NonParallelPlanReason, элемента QueryPlan.
Также, появился элемент OptimizerHardwareDependentProperties, в нем нас интересует атрибут EstimatedAvailableDegreeOfParallelism.
Отмечу, что если вы также как и я, пока пользуетесь SSMS 2008, при работе с 2012 сервером, то не пользуйтесь графическим интерфейсом просмотра планов (даже если потом вы переключитесь в xml). Т.е. не включаете режим Include actual Execution Plan, и не пользуйтесь Display Estimated Execution Plan.
Вместо этого, можно включить режим или задействовать инструменты, перечисленные ниже:

set showplan_xml on --получить оценочный план
set statistics xml on --получить действительный план
sys.dm_exec_query_plan --вытащить запрос из кэша
--Profiler --использовать профайлер

Получив план одним из этих способов, посмотрите содержимое xml, вы увидите искомые атрибуты.
В частности, выполняя запросы с флагом 8649 на редакции Express, я не получил параллельного плана, зато получил такое объяснение:

<QueryPlan NonParallelPlanReasonNoParallelPlansInDesktopOrExpressEdition« …/>

Если попробовать выполнить запрос со скалярной функцией, о котором говорилось выше, то причина указывается просто:

<QueryPlan NonParallelPlanReasonCouldNotGenerateValidParallelPlan« …/>

Не очень информативно.
Ранее мы использовали недокументированную команду dbcc optimizer_whatif. Теперь, можно видеть как ее выполнение отражается в плане.
Если выполнить dbcc optimizer_watif (1,1), или dbcc optimizer_whatif(‘CPUs’,1), что одно и то же, и посмотреть на атрибут OptimizerHardwareDependentProperties. То можно увидеть

dbcc optimizer_whatif('CPUs',1)

<OptimizerHardwareDependentProperties EstimatedAvailableDegreeOfParallelism1» />

dbcc optimizer_whatif('CPUs',8)

<OptimizerHardwareDependentProperties EstimatedAvailableDegreeOfParallelism4» />

Потом, не забудьте сбросить результаты экспериментов.

dbcc optimizer_whatif('ResetAll')

Использованная литература

— Microsoft SQL Server 2008 Internals (Kalen Delany)
— Counting, Enumerating, and Sampling of Execution Plans in a Cost-Based Query Optimizer (Florian Waas, Cesar Galindo-Legaria)
Inside the SQL Server Query Optimizer (Benjamin Nevarez) (можно скачать PDF)
— Microsoft SQL Server 7.0 Query Processor (Goetz Graefe, Jim Ewel, and Cesar Galindo-Legaria)
— Inside Microsoft SQL Server 2008 — TSQL Querying (Itzik Ben Gan)
Блог Craig Freedman-а
Статья: Forcing a Parallel Query Execution Plan

Другие ссылки по этой теме

Inside the Optimizer: Constructing a Plan — Part 1

upd. 28.04.2012
Benjamin Nevarez blog — Inside the Query Optimizer Memo Structure
Benjamin Nevarez blog — More Undocumented Query Optimizer Trace Flags
Paul White blog — Query Optimizer Deep Dive – Part 1

a href=»http://www.somewheresomehow.ru/wp-content/uploads/2012/03/14.jpg»td bgcolor=»#B9CCDF»/atd bgcolor=»#B9CCDF»

 

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Анти-спам: введите результат (цифрами) *