为啥 SQL Server 表值函数插入需要很长时间?
Posted
技术标签:
【中文标题】为啥 SQL Server 表值函数插入需要很长时间?【英文标题】:Why is SQL Server table-valued function insert taking very long?为什么 SQL Server 表值函数插入需要很长时间? 【发布时间】:2013-02-05 07:05:56 【问题描述】:我有一个非常复杂的查询,但我应用了一些索引,现在它在不到 1 秒的时间内运行得非常顺利。查询的结构是这样的(我发现没有必要发布完整的查询,我稍后会证明 - 错误不在查询本身):
DECLARE @period varchar(6);
SET @period = '201302';
DECLARE @day datetime;
SET @day = dba.fnu_firstdate(@period);//returns 2013-02-01
SELECT
user_id,
(SELECT CAST(MAX(c1) AS varchar) FROM table t WHERE t.user_id = table.user_id AND when = DATEADD(day, 0, @day)) Day01,
...
(SELECT CAST(MAX(c1) AS varchar) FROM table t WHERE t.user_id = table.user_id AND when = DATEADD(day, 30, @day)) Day31
FROM
table
是的,如果我执行此查询,大约需要 1 秒才能完成,这对我来说非常好。但是,如您所见,我需要为其提供参数。因此,我将其更改为表值函数,以便可以轻松地从中进行选择查询:
CREATE FUNCTION fnu_data(@period varchar(6))
RETURNS @results TABLE
(
id int,
Day01 varchar(10) null,
...
Day31 varchar(10) null
)
AS
BEGIN
DECLARE @day datetime;
SET @day = dba.fnu_firstdate(@period);
INSERT INTO @results
(
id,
Day01,
...
Day31
)
SELECT
SELECT
user_id,
(SELECT CAST(MAX(c1) AS varchar) FROM table t WHERE t.user_id = table.user_id AND when = DATEADD(day, 0, @day)) Day01,
...
(SELECT CAST(MAX(c1) AS varchar) FROM table t WHERE t.user_id = table.user_id AND when = DATEADD(day, 30, @day)) Day31
FROM
table
RETURN
当我这样做的时候
SELECT * FROM dba.fnu_data('201302')
需要 6 秒,这太长了。在同事的建议下,我尝试在 id 上添加主索引并将每个子选择替换为连接,但它将执行查询的时间延长到 8 秒。 (PS 查询返回约 3200 行)。
在我看来,罪魁祸首是插入,但我不知道如何才能摆脱它。
我可以做些什么来改进我的查询?
【问题讨论】:
你能不能给我更多的细节(也许把它放在答案中?)?我不明白如何将函数参数(我返回的表)移到函数之外? 也许仅将INSERT ... SELECT
的计划与底层SELECT
的计划进行比较就可以揭示一些东西。无论如何,您的 SELECT 语句对我来说显然不是最理想的。
我并不声称我的查询是完美的,但它执行得相当快,至少对我来说足够快。我刚刚尝试创建一个临时表并插入我在函数中插入的方式,它再次运行得非常好(1秒)!现在我完全不知道问题出在哪里。
【参考方案1】:
不确定是什么可能导致独立 SELECT
和 INSERT ... SELECT
作为函数的一部分的性能差异,但我可以建议重写您的 SELECT 语句,因为您的 SELECT 对我来说看起来绝对不是最佳的。
您似乎正在做一个支点,SQL Server 2005+ 中有一个本机语法。考虑以下查询:
WITH data AS (
SELECT
user_id,
DAY([when]) AS day,
c1
FROM [table] t
CROSS APPLY (
SELECT CAST(@period + '01' AS date) -- this is supposed to be a replacement
-- for dba.fnu_firstdate(), but you
-- could use your function here instead
) x (startdate)
WHERE t.day >= x.startdate
AND t.day < DATEADD(MONTH, 1, startdate)
)
INSERT INTO @results
(
id,
Day01,
...
Day31
)
SELECT
id,
[1],
...
[31]
FROM data
PIVOT (
MAX(c1) FOR day IN ([1], [2], ..., [30], [31])
) p
;
它使用公用表表达式将指定月份的数据作为单独的步骤准备好,然后使用 PIVOT 语法通过聚合对结果进行透视。
请注意,上面使用单个语句完成整个工作,这也是一个 SELECT 语句。这意味着您可以将多语句 TVF 转换为 内联 TVF:
IF OBJECT_ID('dba.fnu_data') IS NOT NULL
DROP FUNCTION dba.fnu_data
GO
CREATE FUNCTION dba.fnu_data(@period varchar(6))
RETURNS TABLE
RETURN (
WITH data AS (
SELECT
user_id,
DAY([when]) AS day,
c1
FROM [table] t
CROSS APPLY (
SELECT CAST(@period + '01' AS date)
) x (startdate)
WHERE t.day >= x.startdate
AND t.day < DATEADD(MONTH, 1, startdate)
)
SELECT
id,
CAST([1] AS varchar(30)) AS Day01,
...
CAST([31] AS varchar(30)) AS Day31
FROM data
PIVOT (
MAX(c1) FOR day IN ([1], [2], ..., [30], [31])
) p
)
GO
内联 TVF 在多语句 TVF 之前的优势在于,它的计划是根据调用函数的整个查询来选择的。在这方面,内嵌 TVF 就像一个视图。
请注意,转换必须使用DROP
+ CREATE
完成,就像上面一样,因为多语句 TVF 和内联 TVF 在 SQL Server 中是不同类型的对象,并且可以'不要被改变成另一个。
【讨论】:
我的第一个想法是:这是什么魔法? :D 然后我更换了字段以满足我的需要,它立即起作用。是的。高级编程技能是魔法。我唯一缺少的是,如果用户当月没有数据,则根本不会返回,但我可能会很容易地解决这个问题。我今天学到的是,要成为一名优秀的程序员,我仍然需要学习很多东西。很棒的工作,Andriy M。谢谢!【参考方案2】:我认为您的问题不是物理插入...而是插入的最终选择。
此特定查询将成为您的瓶颈,因为它执行至少 30 次单独的 SELECT 以生成结果集,然后您将其填充回您的表变量中。
SELECT
user_id,
(SELECT CAST(MAX(c1) AS varchar) FROM table t WHERE t.user_id = table.user_id AND when = DATEADD(day, 0, @day)) Day01,
...
(SELECT CAST(MAX(c1) AS varchar) FROM table t WHERE t.user_id = table.user_id AND when = DATEADD(day, 30, @day)) Day31
FROM
table
你能解释一下你想从这个查询中实现什么吗?
【讨论】:
如果简而言之:我正在为作为参数给出的月份的每一天选择某个用户的最高值。正如我在问题中所说,我在函数之外执行完全相同的查询,它运行时间不到 1 秒。现在我将它移到函数中,这需要 6 秒,对我来说很清楚,函数中的某些东西导致了问题。但是,我看到的唯一明显区别是插入语句,这就是为什么我认为这是它的错。 简而言之:查询之间的区别在于一个在函数内,另一个在函数外。 31 个子选择(我做了适当的索引)根本不需要太多时间。以上是关于为啥 SQL Server 表值函数插入需要很长时间?的主要内容,如果未能解决你的问题,请参考以下文章
为啥需要以只读方式输入 SQL Server 存储过程的表值参数?
通过需要很长时间的访问将记录插入到 SQL Server 链接表中[重复]
表值函数从 2012 年迁移到 sql 2017 后性能下降