我正在尝试查看是否存在一种诱使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运行〜500ms
- 查询1运行〜1500ms
- 查询2运行〜1500ms
- 查询3的运行时间约为2000毫秒。
计划2使用索引扫描而不是搜索,然后由嵌套循环上的谓词过滤。计划3并没有那么糟糕,但仍然比计划0的工作量更大,工作更慢。
假设dbo.Params
更改很少,通常有1-200行,最多不超过2000行。现在大约有10列,我不希望增加太多列。
Params中的行数不是固定的,因此每@session_id
行都会有一行。列数不固定,这是我不想dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
从任何地方调用的原因之一,因此我可以在内部向此查询添加新列。即使有一些限制,我也很高兴听到对此的任何意见/建议。