为啥 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】:

不确定是什么可能导致独立 SELECTINSERT ... 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 后性能下降

SQL Server 中标量、表值和聚合函数之间的区别?

通过在 SQL Server 2000 中传递变量参数来加入表值函数

SQL Server 中的存储过程,它采用表值参数并插入不存在的并返回存在的