CLR 用户定义聚合 - 调用函数
适用范围:SQL Server
在 Transact-SQL SELECT 语句中,可以调用公共语言运行时(CLR)用户定义的聚合,但受适用于系统聚合函数的所有规则的约束。
应遵循下列附加规则:
当前用户必须对用户定义的聚合具有 EXECUTE 权限。
必须使用 schema_name.udagg_name 形式的两部分名称调用用户定义的聚合。
用户定义的聚合的参数类型必须与 CREATE AGGREGATE 语句中定义的聚合input_type匹配或隐式转换为聚合的input_type。
用户定义的聚合的返回类型必须与 CREATE AGGREGATE 语句中的return_type匹配。
示例 1
以下是一个用户定义聚合函数的示例,该函数从某个表的一列中获取一组字符串值,然后将这些值连接起来:
[C#]
using System;
using System.Data;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.IO;
using System.Text;
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined, //use clr serialization to serialize the intermediate result
IsInvariantToNulls = true, //optimizer property
IsInvariantToDuplicates = false, //optimizer property
IsInvariantToOrder = false, //optimizer property
MaxByteSize = 8000) //maximum size in bytes of persisted value
]
public class Concatenate : IBinarySerialize
{
/// <summary>
/// The variable that holds the intermediate result of the concatenation
/// </summary>
public StringBuilder intermediateResult;
/// <summary>
/// Initialize the internal data structures
/// </summary>
public void Init()
{
this.intermediateResult = new StringBuilder();
}
/// <summary>
/// Accumulate the next value, not if the value is null
/// </summary>
/// <param name="value"></param>
public void Accumulate(SqlString value)
{
if (value.IsNull)
{
return;
}
this.intermediateResult.Append(value.Value).Append(',');
}
/// <summary>
/// Merge the partially computed aggregate with this aggregate.
/// </summary>
/// <param name="other"></param>
public void Merge(Concatenate other)
{
this.intermediateResult.Append(other.intermediateResult);
}
/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation.
/// </summary>
/// <returns></returns>
public SqlString Terminate()
{
string output = string.Empty;
//delete the trailing comma, if any
if (this.intermediateResult != null
&& this.intermediateResult.Length > 0)
{
output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1);
}
return new SqlString(output);
}
public void Read(BinaryReader r)
{
intermediateResult = new StringBuilder(r.ReadString());
}
public void Write(BinaryWriter w)
{
w.Write(this.intermediateResult.ToString());
}
}
[Visual Basic]
Imports System
Imports System.Data
Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes
Imports System.IO
Imports System.Text
<Serializable(), SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToNulls:=True, IsInvariantToDuplicates:=False, IsInvariantToOrder:=False, MaxByteSize:=8000)> _
Public Class Concatenate
Implements IBinarySerialize
''' <summary>
''' The variable that holds the intermediate result of the concatenation
''' </summary>
Public intermediateResult As StringBuilder
''' <summary>
''' Initialize the internal data structures
''' </summary>
Public Sub Init()
Me.intermediateResult = New StringBuilder()
End Sub
''' <summary>
''' Accumulate the next value, not if the value is null
''' </summary>
''' <param name="value"></param>
Public Sub Accumulate(ByVal value As SqlString)
If value.IsNull Then
Return
End If
Me.intermediateResult.Append(value.Value).Append(","c)
End Sub
''' <summary>
''' Merge the partially computed aggregate with this aggregate.
''' </summary>
''' <param name="other"></param>
Public Sub Merge(ByVal other As Concatenate)
Me.intermediateResult.Append(other.intermediateResult)
End Sub
''' <summary>
''' Called at the end of aggregation, to return the results of the aggregation.
''' </summary>
''' <returns></returns>
Public Function Terminate() As SqlString
Dim output As String = String.Empty
'delete the trailing comma, if any
If Not (Me.intermediateResult Is Nothing) AndAlso Me.intermediateResult.Length > 0 Then
output = Me.intermediateResult.ToString(0, Me.intermediateResult.Length - 1)
End If
Return New SqlString(output)
End Function
Public Sub Read(ByVal r As BinaryReader) Implements IBinarySerialize.Read
intermediateResult = New StringBuilder(r.ReadString())
End Sub
Public Sub Write(ByVal w As BinaryWriter) Implements IBinarySerialize.Write
w.Write(Me.intermediateResult.ToString())
End Sub
End Class
将代码编译为 MyAgg.dll后,可以在 SQL Server 中注册聚合,如下所示:
CREATE ASSEMBLY MyAgg FROM 'C:\MyAgg.dll';
GO
CREATE AGGREGATE MyAgg (@input nvarchar(200)) RETURNS nvarchar(max)
EXTERNAL NAME MyAgg.Concatenate;
注意
SQL Server 中不支持使用 /clr:pure 编译器选项编译的视觉C++数据库对象(如标量值函数)。
与大多数聚合一样,大部分逻辑位于 Aggregate 方法中。 在这里,作为参数传入的 StringBuilder 对象追加到 Init 方法中初始化的 StringBuilder 对象。 假设这不是首次 调用 Accumulate 方法时,在追加传入字符串之前,还会将逗号追加到 StringBuilder 。 在计算任务结束时, 将调用 Terminate 方法,该方法将 StringBuilder 作为字符串返回。
例如,请考虑具有以下架构的表:
CREATE TABLE BookAuthors
(
BookID int NOT NULL,
AuthorName nvarchar(200) NOT NULL
);
然后插入以下行:
INSERT BookAuthors VALUES(1, 'Johnson'),(2, 'Taylor'),(3, 'Steven'),(2, 'Mayler'),(3, 'Roberts'),(3, 'Michaels');
随后,以下查询将生成以下结果:
SELECT BookID, dbo.MyAgg(AuthorName)
FROM BookAuthors
GROUP BY BookID;
BookID | Author Names |
---|---|
1 | Johnson |
2 | Taylor, Mayler |
3 | Roberts, Michaels, Steven |
示例 2
下面的示例演示了一个聚合,该聚合在 Aggregate 方法上具有两个参数。
[C#]
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
[Serializable]
[SqlUserDefinedAggregate(
Format.Native,
IsInvariantToDuplicates = false,
IsInvariantToNulls = true,
IsInvariantToOrder = true,
IsNullIfEmpty = true,
Name = "WeightedAvg")]
public struct WeightedAvg
{
/// <summary>
/// The variable that holds the intermediate sum of all values multiplied by their weight
/// </summary>
private long sum;
/// <summary>
/// The variable that holds the intermediate sum of all weights
/// </summary>
private int count;
/// <summary>
/// Initialize the internal data structures
/// </summary>
public void Init()
{
sum = 0;
count = 0;
}
/// <summary>
/// Accumulate the next value, not if the value is null
/// </summary>
/// <param name="Value">Next value to be aggregated</param>
/// <param name="Weight">The weight of the value passed to Value parameter</param>
public void Accumulate(SqlInt32 Value, SqlInt32 Weight)
{
if (!Value.IsNull && !Weight.IsNull)
{
sum += (long)Value * (long)Weight;
count += (int)Weight;
}
}
/// <summary>
/// Merge the partially computed aggregate with this aggregate
/// </summary>
/// <param name="Group">The other partial results to be merged</param>
public void Merge(WeightedAvg Group)
{
sum += Group.sum;
count += Group.count;
}
/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation.
/// </summary>
/// <returns>The weighted average of all inputed values</returns>
public SqlInt32 Terminate()
{
if (count > 0)
{
int value = (int)(sum / count);
return new SqlInt32(value);
}
else
{
return SqlInt32.Null;
}
}
}
[Visual Basic]
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.SqlTypes
Imports Microsoft.SqlServer.Server
Imports System.Runtime.InteropServices
<StructLayout(LayoutKind.Sequential)> _
<Serializable(), SqlUserDefinedAggregate(Format.Native, _
IsInvariantToDuplicates:=False, _
IsInvariantToNulls:=True, _
IsInvariantToOrder:=True, _
IsNullIfEmpty:=True, _
Name:="WeightedAvg")> _
Public Class WeightedAvg
''' <summary>
''' The variable that holds the intermediate sum of all values multiplied by their weight
''' </summary>
Private sum As Long
''' <summary>
''' The variable that holds the intermediate sum of all weights
''' </summary>
Private count As Integer
''' <summary>
''' The variable that holds the intermediate sum of all weights
''' </summary>
Public Sub Init()
sum = 0
count = 0
End Sub
''' <summary>
''' Accumulate the next value, not if the value is null
''' </summary>
''' <param name="Value">Next value to be aggregated</param>
''' <param name="Weight">The weight of the value passed to Value parameter</param>
Public Sub Accumulate(ByVal Value As SqlInt32, ByVal Weight As SqlInt32)
If Not Value.IsNull AndAlso Not Weight.IsNull Then
sum += CType(Value, Long) * CType(Weight, Long)
count += CType(Weight, Integer)
End If
End Sub
''' <summary>
''' Merge the partially computed aggregate with this aggregate.
''' </summary>
''' <param name="Group">The other partial results to be merged</param>
Public Sub Merge(ByVal Group As WeightedAvg)
sum = Group.sum
count = Group.count
End Sub
''' <summary>
''' Called at the end of aggregation, to return the results of the aggregation.
''' </summary>
''' <returns>The weighted average of all inputed values</returns>
Public Function Terminate() As SqlInt32
If count > 0 Then
'' int value = (int)(sum / count);
'' return new SqlInt32(value);
Dim value As Integer = CType(sum / count, Integer)
Return New SqlInt32(value)
Else
Return SqlInt32.Null
End If
End Function
End Class
编译 C# 或 Visual Basic 源代码后,运行以下 Transact-SQL。 此脚本假设相应的 DLL 名为 WghtAvg.dll,并且位于 C 驱动器的根目录下, 还假设数据库名为 test。
use test;
go
-- sp_configure 'clr enabled', 1;
-- go
--- RECONFIGURE WITH OVERRIDE;
-- go
IF EXISTS (SELECT name FROM systypes WHERE name = 'MyTableType')
DROP TYPE MyTableType;
go
IF EXISTS (SELECT name FROM sysobjects WHERE name = 'WeightedAvg')
DROP AGGREGATE WeightedAvg;
go
IF EXISTS (SELECT name FROM sys.assemblies WHERE name = 'MyClrCode')
DROP ASSEMBLY MyClrCode;
go
CREATE ASSEMBLY MyClrCode FROM 'C:\WghtAvg.dll';
GO
CREATE AGGREGATE WeightedAvg (@value int, @weight int) RETURNS int
EXTERNAL NAME MyClrCode.WeightedAvg;
go
CREATE TYPE MyTableType AS table (ItemValue int, ItemWeight int);
go
DECLARE @myTable AS MyTableType;
INSERT INTO @myTable VALUES(1, 4), (6, 1);
SELECT dbo.WeightedAvg(ItemValue, ItemWeight) FROM @myTable;
go