将表表达式传递给表参数化函数

时间:2014-01-16 20:18:42

标签: sql-server sql-server-2008-r2

我有以下用户定义的功能:

CREATE FUNCTION dbo.GetConcatenatedWithKeyAsInt32(@Values dbo.IndexValue READONLY)
RETURNS TABLE 
WITH SCHEMABINDING
AS 
RETURN (
  SELECT
  [Id],
   -- The definition of [Names] is not really important to this question!
  [Names] = stuff((
    select ', ' + Value
    from @Values AS xt
    where xt.Id = t.Id
    for xml path(''), TYPE
  ).value('.[1]','varchar(max)'), 1, 2, '')
  FROM @Values AS t GROUP BY t.Id);

该参数属于用户定义的表类型:

CREATE TYPE IndexValue AS TABLE ( Id int, Value VARCHAR(max) );

我很难调用这个功能。

我找到了人们在实际的物理表(或视图)上调用这样一个函数的例子,但是肯定可以直接在select表达式中使用它,不是吗?

我试过了:

SELECT *
FROM dbo.GetConcatenatedWithKeyAsInt32(
  SELECT c.Id AS Id, a.City AS value
  FROM Customers c
  JOIN Addresses a ON c.Id = a.CustomerId
);

SQL Server不喜欢这样:

Incorrect syntax near the keyword 'SELECT'.
Incorrect syntax near ')'

这可能吗?如果是这样,那么正确的语法是什么?

或者我真的需要为

创建临时表或视图
SELECT c.Id AS Id, a.City AS value
FROM Customers c
JOIN Addresses a ON c.Id = a.CustomerId

第一

2 个答案:

答案 0 :(得分:16)

不可以通过引用传递查询表达式而不是通过值传递(您尝试使用的语法完全不受支持)。所以,如果这是你问题的要点,那么答案是

但请允许我补充一些关于你如何能够和/或应该完成你想要完成的事情的其他建议。

首先,在创建或引用对象时请always use the schema prefix

CREATE TYPE dbo.IndexValue AS TABLE ( Id int, Value VARCHAR(max) );

接下来,使用内联表值函数而不是多语句表值函数,使用SCHEMABINDINGalways specify a length for variable-length types like nvarchar(虽然不确定为什么使用nvarchar在函数中,当输入不可能时:)

CREATE FUNCTION dbo.GetConcatenatedWithKeyAsInt32
(
  @Values dbo.IndexValue READONLY
)
RETURNS TABLE
WITH SCHEMABINDING
AS 
  RETURN (SELECT
    [Id],
    [Names] = stuff((
    select ', ' + CAST(Id AS nvarchar(255)) as [text()]
    from @Values AS xt
    where xt.Id = t.Id
    for xml path('')
    ), 1, 2, '')
  FROM @Values AS t GROUP BY t.Id);
GO

现在你可以使用这个功能没问题:

DECLARE @x dbo.IndexValue;
INSERT @x VALUES(1,'hoobah'),(1,'floobah'),(2,'a'),(2,'x'),(2,'y'),(3,'me');
SELECT Id, Names FROM dbo.GetConcatenatedWithKeyAsInt32(@x);

虽然我不得不怀疑你的函数中还有另一个逻辑错误,那就是你应该将XML操作应用于Value列,而不是ID。输出结果如下:

Id   Names
---- -------
1    1,1
2    2,2,2
3    3

如果这是您实际要做的事情,那么该函数应该有进一步的更改,最重要的是纠正输入处理,并保护数据不受XML不安全的实体(例如>)。

ALTER FUNCTION dbo.GetConcatenatedWithKeyAsInt32
(
  @Values dbo.IndexValue READONLY
)
RETURNS TABLE
WITH SCHEMABINDING
AS 
  RETURN (SELECT
    [Id],
    [Names] = stuff((
    select ', ' + Value
    from @Values AS xt
    where xt.Id = t.Id
    for xml path(''), TYPE
    ).value('.[1]','varchar(max)'), 1, 2, '')
  FROM @Values AS t GROUP BY t.Id);
GO

现在这样做效果更好:

DECLARE @x dbo.IndexValue;
INSERT @x VALUES(1,'hoo&bah'),(1,'floo<b>ah'),(2,'a'),(2,'x'),(2,'y'),(3,'me');
SELECT Id, Names FROM dbo.GetConcatenatedWithKeyAsInt32(@x);

输出:

Id   Names
---- ------------------
1    hoo&bah, floo<b>ah
2    a, x, y
3    me

至于错误消息,@ Lamak是正确的,你不能将查询传递给函数的参数,你需要在查询中包含函数调用,例如使用CROSS APPLY或OUTER APPLY。

至于你试图开始工作的更新代码示例,我不知道你为什么要使用这个函数。为什么不呢:

SELECT DISTINCT c.Id, Names = STUFF((SELECT ', ' + a.City
     FROM dbo.Addresses AS a
     WHERE a.CustomerId = c.Id GROUP BY a.City
     FOR XML PATH(''), TYPE
).value('.[1]','varchar(max)'), 1, 2, '')
FROM dbo.Customers AS c;

如果你试图让它变得通用,那么我想你可能会回到你效率低下的多语句TVF。以下是执行此操作的语法:

CREATE FUNCTION dbo.GetConcatenatedWithKeyAsInt32_b
(
  @Values dbo.IndexValue READONLY
)
RETURNS @ret TABLE
(
   Id int PRIMARY KEY NOT NULL, 
   value nvarchar(max) NOT NULL
)
WITH SCHEMABINDING
AS 
BEGIN
  INSERT @ret SELECT
    Id, Names = STUFF((SELECT ', ' + Value
     FROM @Values AS xt 
     WHERE xt.Id = t.Id GROUP BY Value
     FOR XML PATH(''), TYPE
    ).value('.[1]','varchar(max)'), 1, 2, '')
  FROM @Values AS t GROUP BY t.id;

  RETURN;
END
GO

然后你可以调用它 - 再次,因为你想要将通用表类型作为输入 - 在将连接的结果填充到声明的表变量之后。抱歉,没办法只是将SQL查询传递给函数。您需要传递一个表,而不是查询。

DECLARE @x dbo.IndexValue;

INSERT @x(Id, Value) 
  SELECT c.Id, a.City
  FROM dbo.Addresses AS a
  INNER JOIN dbo.Customers AS c
  ON a.CustomerId = c.Id;

SELECT * INTO #x FROM dbo.GetConcatenatedWithKeyAsInt32_b(@x);

除了混淆XML逻辑之外,这并没有真正获得任何东西,并且可能会大大减慢这个查询速度。你似乎没有明显的理由经历了很多额外的层(将连接的结果插入表类型,然后使用表类型来调用函数,然后将这些行插入到函数内的声明表中,然后对该表执行XML操作)。

不要查看执行计划以查看哪个“更快” - 它会分配各种近似成本,并且不知道如何正确评估和花费某些XML操作。相反,只是把它们计时!

SET NOCOUNT ON;
SELECT GETUTCDATE();
GO

    SELECT c.Id, Names = STUFF((SELECT ', ' + a.City
         FROM dbo.Addresses AS a
         WHERE a.CustomerId = c.Id GROUP BY a.City
         FOR XML PATH(''), TYPE
    ).value('.[1]','varchar(max)'), 1, 2, '')
    INTO #x
    FROM dbo.Customers AS c;

    DROP TABLE #x;

GO 5000
SELECT GETUTCDATE();
GO
    DECLARE @x dbo.IndexValue;

    INSERT @x(Id, Value) 
      SELECT c.Id, a.City
      FROM dbo.Addresses AS a
      INNER JOIN dbo.Customers AS c
      ON a.CustomerId = c.Id;

    SELECT * INTO #x FROM dbo.GetConcatenatedWithKeyAsInt32_b(@x);
    DROP TABLE #x;
GO 5000
SELECT GETUTCDATE();
GO

结果:

XML:           16,780 ms
Your approach: 32,230 ms

即使我改变了:

    INSERT @x(Id, Value) 
      SELECT c.Id, a.City

要:

    INSERT @x(Id, Value) 
      SELECT DISTINCT c.Id, a.City

(因此该函数处理的数据较少,如果有重复),我只需要休息一下(你仍需要函数中的GROUP BY,恕我直言,以保护自己免受查询会喂它):

XML:           16,824 ms
Your approach: 29,576 ms
当我告诉你DRY在SQL Server中并不总是有益时,我并没有把事情搞砸。函数不仅仅是指针,它们还有很大的开销。

答案 1 :(得分:8)

回答您的具体问题:

  

这可能吗?如果是这样,那么正确的语法是什么?或者我真的需要先创建临时表或视图吗?

SQL Server不支持您尝试执行此操作的方式。 的方式是分配用户定义类型的变量,并将SELECT值放入该变量中:

declare @values as dbo.IndexValue

insert into @values
SELECT c.Id AS Id, a.City AS value
  FROM Customers c
  JOIN Addresses a ON c.Id = a.CustomerId

select * from dbo.GetConcatenatedWithKeyAsInt32(@values)

我相信自SQL Server 2008起支持此功能。

它可能与创建临时表类似,但我相信这是实现您所要求的唯一方法。