Я пытаюсь увидеть, есть ли способ обмануть SQL Server, чтобы использовать определенный план для запроса.
1. Окружающая среда
Представьте, что у вас есть данные, которые используются разными процессами. Итак, предположим, у нас есть результаты экспериментов, которые занимают много места. Затем для каждого процесса мы знаем, какой год / месяц результата эксперимента мы хотим использовать.
if object_id('dbo.SharedData') is not null
drop table SharedData
create table dbo.SharedData (
experiment_year int,
experiment_month int,
rn int,
calculated_number int,
primary key (experiment_year, experiment_month, rn)
)
go
Теперь для каждого процесса у нас есть параметры, сохраненные в таблице.
if object_id('dbo.Params') is not null
drop table dbo.Params
create table dbo.Params (
session_id int,
experiment_year int,
experiment_month int,
primary key (session_id)
)
go
2. Тестовые данные
Давайте добавим некоторые тестовые данные:
insert into dbo.Params (session_id, experiment_year, experiment_month)
select 1, 2014, 3 union all
select 2, 2014, 4
go
insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
2014, 3, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
cross join master.dbo.spt_values as v2
go
insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
2014, 4, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
cross join master.dbo.spt_values as v2
go
3. Получение результатов
Теперь получить результаты эксперимента очень просто @experiment_year/@experiment_month
:
create or alter function dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.SharedData as d
where
d.experiment_year = @experiment_year and
d.experiment_month = @experiment_month
)
go
План приятный и параллельный:
select
calculated_number,
count(*)
from dbo.f_GetSharedData(2014, 4)
group by
calculated_number
план запроса 0
4. Проблема
Но, чтобы сделать использование данных более общим, я хочу иметь другую функцию - dbo.f_GetSharedDataBySession(@session_id int)
. Итак, простым способом было бы создать скалярные функции, переводя @session_id
-> @experiment_year/@experiment_month
:
create or alter function dbo.fn_GetExperimentYear(@session_id int)
returns int
as
begin
return (
select
p.experiment_year
from dbo.Params as p
where
p.session_id = @session_id
)
end
go
create or alter function dbo.fn_GetExperimentMonth(@session_id int)
returns int
as
begin
return (
select
p.experiment_month
from dbo.Params as p
where
p.session_id = @session_id
)
end
go
И теперь мы можем создать нашу функцию:
create or alter function dbo.f_GetSharedDataBySession1(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.f_GetSharedData(
dbo.fn_GetExperimentYear(@session_id),
dbo.fn_GetExperimentMonth(@session_id)
) as d
)
go
план запроса 1
План такой же, за исключением того, что он, конечно, не параллельный, потому что скалярные функции, выполняющие доступ к данным, делают весь план последовательным .
Поэтому я попробовал несколько разных подходов, например, используя подзапросы вместо скалярных функций:
create or alter function dbo.f_GetSharedDataBySession2(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.f_GetSharedData(
(select p.experiment_year from dbo.Params as p where p.session_id = @session_id),
(select p.experiment_month from dbo.Params as p where p.session_id = @session_id)
) as d
)
go
план запроса 2
Или используя cross apply
create or alter function dbo.f_GetSharedDataBySession3(@session_id int)
returns table
as
return (
select
d.rn,
d.calculated_number
from dbo.Params as p
cross apply dbo.f_GetSharedData(
p.experiment_year,
p.experiment_month
) as d
where
p.session_id = @session_id
)
go
план запроса 3
Но я не могу найти способ написать этот запрос так же хорошо, как тот, который использует скалярные функции.
Пара мыслей:
- По сути, мне бы хотелось иметь возможность каким-то образом указывать SQL Server предварительно вычислять определенные значения, а затем передавать их как константы.
- Что может быть полезным, если бы у нас был некоторый промежуточный намек на материализацию . Я проверил несколько вариантов (TVF с несколькими утверждениями или cte с верхом), но ни один план пока не так хорош, как план со скалярными функциями.
- Я знаю о предстоящем улучшении SQL Server 2017 - Froid: оптимизация императивных программ в реляционной базе данных. Однако я не уверен, что это поможет. Хотя было бы неплохо оказаться здесь неправым.
Дополнительная информация
Я использую функцию (а не выбираю данные непосредственно из таблиц), потому что ее гораздо проще использовать во многих различных запросах, которые обычно имеют @session_id
параметр.
Меня попросили сравнить фактическое время выполнения. В этом конкретном случае
- запрос 0 выполняется в течение ~ 500 мс
- запрос 1 выполняется в течение ~ 1500 мс
- запрос 2 выполняется в течение ~ 1500 мс
- запрос 3 выполняется в течение ~ 2000 мс.
План № 2 имеет поиск индекса вместо поиска, который затем фильтруется по предикатам во вложенных циклах. План № 3 не так уж и плох, но все же выполняет больше работы и работает медленнее, чем план № 0.
Давайте предположим, что dbo.Params
это изменяется редко, и обычно имеет около 1-200 строк, не больше, скажем, 2000 когда-либо ожидается. Сейчас это около 10 столбцов, и я не собираюсь добавлять столбцы слишком часто.
Количество строк в Params не фиксировано, поэтому для каждого @session_id
будет ряд. Количество столбцов там не фиксировано, это одна из причин, по которой я не хочу звонить dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
откуда угодно , поэтому я могу добавить новый столбец к этому запросу внутри страны. Я был бы рад услышать любые мнения / предложения по этому вопросу, даже если у него есть некоторые ограничения.
источник
Ответы:
Вы не можете действительно безопасно достичь именно того, что вы хотите в SQL Server сегодня, то есть с помощью одного оператора и с параллельным выполнением, в рамках ограничений, изложенных в вопросе (как я их понимаю).
Поэтому мой простой ответ - нет . Остальная часть этого ответа - в основном обсуждение того, почему это так, если оно представляет интерес.
Как указано в вопросе, можно получить параллельный план, но есть два основных варианта, ни один из которых не подходит для ваших нужд:
Коррелированные вложенные циклы объединяются с циклическим распределением потоков на верхнем уровне. Учитывая, что
Params
для определенногоsession_id
значения гарантированно может быть одна строка , внутренняя сторона будет работать в одном потоке, даже если она помечена значком параллелизма. Вот почему, очевидно, параллельный план 3 не работает так же хорошо; это на самом деле серийный.Другая альтернатива - для независимого параллелизма на внутренней стороне соединения вложенных циклов. Независимость здесь означает, что потоки запускаются на внутренней стороне, а не просто на тех же потоках, которые выполняют внешнюю сторону соединения вложенных циклов. SQL Server поддерживает независимый параллелизм вложенных циклов внутренней стороны только тогда, когда гарантируется наличие одной строки внешней стороны и нет соответствующих параметров соединения ( план 2 ).
Итак, у нас есть выбор параллельного плана, который является последовательным (из-за одного потока) с желаемыми коррелированными значениями; или параллельный план внутренней стороны, который должен сканироваться, потому что у него нет параметров для поиска. (Помимо: на самом деле должно быть разрешено управлять внутренним параллелизмом, используя ровно один набор коррелированных параметров, но он никогда не был реализован, вероятно, по уважительной причине).
Тогда возникает естественный вопрос: зачем вообще нужны коррелированные параметры? Почему SQL Server не может просто напрямую искать скалярные значения, предоставляемые, например, подзапросом?
Что ж, SQL Server может только «поиск по индексу», используя простые скалярные ссылки, например, константу, переменную, столбец или ссылку на выражение (так что результат скалярной функции также может быть определен). Подзапрос (или другая подобная конструкция) просто слишком сложен (и потенциально небезопасен), чтобы вставить его в целое ядро хранилища. Таким образом, отдельные операторы плана запроса не требуется. Это поворот требует корреляции, что означает отсутствие параллелизма того типа, который вы хотите.
В общем, в настоящее время действительно нет лучшего решения, чем методы, такие как присвоение значений поиска переменным, а затем их использование в параметрах функции в отдельном выражении.
Теперь у вас могут быть конкретные местные соображения, которые означают, что
SESSION_CONTEXT
стоит кэшировать текущие значения года и месяца, то есть:Но это попадает в категорию обходных путей.
С другой стороны, если производительность агрегирования имеет первостепенное значение, вы можете рассмотреть возможность использования встроенных функций и создания индекса columnstore (первичного или вторичного) для таблицы. В любом случае вы можете обнаружить, что преимущества хранилища columnstore, обработки в пакетном режиме и агрегирования pushdown обеспечивают большие преимущества, чем параллельный поиск в режиме строки.
Но остерегайтесь скалярных функций T-SQL, особенно с хранилищем columnstore, поскольку легко получить функцию, оцениваемую для каждой строки, в отдельном фильтре режима строки. Обычно довольно сложно гарантировать, сколько раз SQL Server выберет оценку скаляров, и лучше не пытаться.
источник
session_context
но решил, что это слишком сумасшедшая идея для меня, и я не уверен, как она будет соответствовать моей нынешней архитектуре. Что может быть полезно, так это, может быть, некоторая подсказка, которую я мог бы использовать, чтобы дать оптимизатору понять, что он должен обрабатывать результат подзапроса как простую скалярную ссылку.Насколько я знаю, желаемая форма плана невозможна только с помощью T-SQL. Кажется, вы хотите, чтобы исходная форма плана (план запроса 0) с подзапросами из ваших функций применялась как фильтры непосредственно к сканированию кластерного индекса. Вы никогда не получите такой план запроса, если не будете использовать локальные переменные для хранения возвращаемых значений скалярных функций. Вместо этого фильтрация будет реализована как соединение с вложенным циклом. Существует три различных способа (с точки зрения параллелизма), которые могут быть реализованы соединением цикла:
Это единственные возможные формы плана, которые мне известны. Вы можете получить другие, если вы используете временную таблицу, но ни одна из них не решит вашу фундаментальную проблему, если вы хотите, чтобы производительность запроса была такой же, как и для запроса 0.
Вы можете добиться эквивалентной производительности запросов, используя скалярные пользовательские функции для назначения возвращаемых значений локальным переменным и используя эти локальные переменные в своем запросе. Вы можете обернуть этот код в хранимую процедуру или UDF с несколькими операторами, чтобы избежать проблем с обслуживаемостью. Например:
Скалярные пользовательские функции были перемещены за пределы запроса, который вы хотите иметь право на параллелизм. План запроса, который я получаю, кажется, тот, который вы хотите:
Оба подхода имеют недостатки, если вам нужно использовать этот набор результатов в других запросах. Вы не можете напрямую присоединиться к хранимой процедуре. Вы должны сохранить результаты во временную таблицу, которая имеет свой собственный набор проблем. Вы можете присоединиться к MS-TVF, но в SQL Server 2016 вы можете столкнуться с проблемами оценки количества элементов. SQL Server 2017 предлагает чередованное выполнение для MS-TVF, которое может полностью решить проблему.
Просто чтобы прояснить несколько вещей: Скалярные пользовательские функции T-SQL всегда запрещают параллелизм, и Microsoft не говорит, что FROID будет доступен в SQL Server 2017.
источник
Скорее всего, это можно сделать с помощью SQLCLR. Одно из преимуществ скалярных пользовательских функций SQLCLR заключается в том, что они не предотвращают параллелизм, если они не осуществляют никакого доступа к данным (а иногда их также необходимо пометить как «детерминированные»). Итак, как вы используете что-то, что не требует доступа к данным, когда сама операция требует доступа к данным?
Ну, потому что
dbo.Params
таблица должна:INT
столбцаэто возможно кэшировать три колонки -
session_id, experiment_year int, experiment_month
- в коллекцию статической (например, словарь, возможно), заполняемый вне процесса и читать скалярную UDF , которые получаютexperiment_year int
иexperiment_month
значение. Я имею в виду «вне процесса»: у вас может быть совершенно отдельный SQLCLR Scalar UDF или хранимая процедура, которая может осуществлять доступ к данным и выполнять чтение изdbo.Params
таблицы для заполнения статической коллекции. Эта UDF или хранимая процедура будет выполняться перед использованием UDF, которые получают значения «year» и «month», таким образом, UDF, которые получают значения «year» и «month», не осуществляют никакого доступа к данным БД.UDF или хранимая процедура, которая читает данные, может сначала проверить, есть ли в коллекции 0 записей, и если да, то заполнить, иначе пропустить. Вы даже можете отслеживать время, в течение которого оно было заполнено, и, если оно прошло более X минут (или что-то в этом роде), то очистить и повторно заполнить, даже если в коллекции есть записи. Но пропуск численности поможет, так как его нужно будет выполнять часто, чтобы гарантировать, что он всегда заполнен для двух основных UDF-значений, из которых можно получить значения.
Основная проблема заключается в том, когда SQL Server решает выгрузить домен приложения по какой-либо причине (или он вызван чем-то, использующим
DBCC FREESYSTEMCACHE('ALL');
). Вы не хотите рисковать очисткой этой коллекции между выполнением UDF «заполнить» или хранимой процедуры и UDF, чтобы получить значения «год» и «месяц». В этом случае вы можете проверить в самом начале этих двух UDF, чтобы генерировать исключение, если коллекция пуста, поскольку лучше ошибиться, чем успешно предоставлять ложные результаты.Конечно, упомянутая выше озабоченность предполагает, что желание состоит в том, чтобы Ассамблея была отмечена как
SAFE
. Если сборка может быть помечена какEXTERNAL_ACCESS
, то можно заставить статический конструктор выполнить метод, который считывает данные и заполняет коллекцию, так что вам когда-либо потребуется выполнить это вручную, чтобы обновить строки, но они всегда будут заполнены (поскольку конструктор статического класса всегда запускается при загрузке класса, что происходит всякий раз, когда метод в этом классе выполняется после перезапуска или выгрузки домена приложения). Это требует использования обычного соединения, а не внутрипроцессного соединения контекста (которое недоступно для статических конструкторов, следовательно, необходимоEXTERNAL_ACCESS
).Обратите внимание: чтобы не требовалось пометить сборку как
UNSAFE
, необходимо пометить любые переменные статического класса какreadonly
. Это означает, по крайней мере, коллекцию. Это не проблема, поскольку в коллекциях, доступных только для чтения, элементы могут быть добавлены или удалены из них, они просто не могут быть инициализированы вне конструктора или начальной загрузки. Отслеживать время загрузки коллекции с целью ее истечения через X минут сложнее, посколькуstatic readonly DateTime
переменная класса не может быть изменена вне конструктора или начальной загрузки. Чтобы обойти это ограничение, вам нужно использовать статическую коллекцию, доступную только для чтения, которая содержит один элемент,DateTime
значение которого можно удалить и повторно добавить при обновлении.источник
readonly statics
в SQLCLR безопасно или разумно. Гораздо меньше я убежден в том, что тогда нужно будет обмануть систему, сделав этотreadonly
ссылочный тип, который вы затем измените . Дает мне абсолютную волю.static
объектов) в SQL Server, да, существует риск возникновения гонки. Вот почему я сначала определил по ОП, что эти данные минимальны и стабильны, и почему я квалифицировал этот подход как требующий «редко меняющихся», и дал средство обновления при необходимости. В этом случае я не вижу большого риска. Несколько лет назад я обнаружил пост о возможности обновлять коллекции только для чтения как разработанные (в C # нет обсуждения re: SQLCLR). Постараюсь найти это.