木野狐 (Neil Chen)

用 C# 开发 SQL Server 2005 的自定义聚合函数

在 SQL 中,经常需要对数据按组进行自定义的聚合操作,比如用逗号连接一系列表示 ID 的数字,但默认只有 SUM, MAX, MIN, AVG 等聚合函数。在 SQL Server 2005 中提供了编写 CLR 的托管代码的支持,我们可以用来写自定义的聚合函数。
比如对于如下数据:

Age Name
20 张三
21 李四
20 王二
22 赵五
18 钱六

我们想得到

Age Name
18 钱六
20 张三,王二
21 李四
22 赵五

需要实现一个聚合函数 StrJoin, 其功能是用逗号连接字符串。
预期的 SQL 语句如下:
select 
    Age, 
    dbo.StrJoin(Name) 
as Name
from 
    SomeTable

要实现这个函数,用 Visual Studio 2005 建立一个 C# 的 Database 项目,项目模版选择 SQL Server 数据库。在项目管理器里添加一个 Aggregate 后,输入代码如下:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Text;

[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.UserDefined, 
    IsInvariantToDuplicates
=false,
    IsInvariantToNulls
=true,
    IsInvariantToOrder
=false,
    IsNullIfEmpty
=true,
    MaxByteSize
=8000
)]
public struct StrJoin: IBinarySerialize {
    
private StringBuilder _result;

    
public void Init() {
        _result 
= new StringBuilder();
    }

    
public void Accumulate(SqlString Value) {
        
if (Value.IsNull) {
            
return;
        } 
else {
            
if (_result.Length > 0)
                _result.Append(
",");
            _result.Append(Value.Value);
        }
    }

    
public void Merge(StrJoin Group) {
        _result.Append(Group._result);
    }

    
public SqlString Terminate() {
        
if (_result.Length > 0) {
            
return new SqlString(_result.ToString());
        }
        
return new SqlString("");
    }

    
#region IBinarySerialize Members

    
public void Read(System.IO.BinaryReader r) {
        _result 
= new StringBuilder(r.ReadString());
    }

    
public void Write(System.IO.BinaryWriter w) {
        w.Write(_result.ToString());
    }

    
#endregion
}

这里不叙述详细的操作步骤,网上应该可以搜到很多。
其原理是该类中提供了几个模版方法:Init(), Accumulate(), Merge(), Terminate().
我们需要做的是在其中写自己的聚合逻辑即可。这几个方法的含义分别是初始化,扫描到一条记录时,合并,终止扫描。

需要注意以下几点:

1. 自定义聚集函数中,我们返回的数据会被序列化然后转换到 SQL Server 中,对一些数值类型 Framework 提供了默认的序列化机制,但其他一些 CLR 的类型比如 string 就必须自己实现序列化机制,也就是实现 IBinarySerialize 接口。

2. 返回值和 SQL Server 里定义的变量一样,受到 8000 字节的长度限制。

3. SQL Server 2005 必须设置兼容性级别为 "SQL Server 2005(90)", 否则会出现如下错误:
 'EXTERNAL' 附近有语法错误。您可能需要将当前数据库的兼容级别设置为更高的值,
以启用此功能。有关存储过程 sp_dbcmptlevel 的信息,请参见帮助。

4. 需要开启 SQL Server 2005 对 CLR 的支持(如果没有打开的话)。
执行如下命令:
EXEC sp_configure 'clr enabled'1
RECONFIGURE WITH OVERRIDE
GO

posted on 2006-11-15 18:53 木野狐(Neil Chen) 阅读(2801) 评论(4)  编辑 收藏 网摘 所属分类: .NETSQL Server

评论

#1楼 2006-11-16 10:36 chuanzai[未注册用户]

这样效率如何?   回复  引用    

#2楼[楼主] 2006-11-16 10:39 木野狐      

@chuanzai
性能还是不错的。不过我没有做压力测试。
  回复  引用  查看    

#3楼 2009-04-17 10:55 夜之悲哀      

老兄,如果要构造两个参数的聚合函数怎么办呢?
微软的方法好像行不通
http://technet.microsoft.com/zh-cn/library/ms131056.aspx" target="_new">http://technet.microsoft.com/zh-cn/library/ms131056.aspx
我试过了,不行
QQ:75211498
  回复  引用  查看    

#4楼[楼主] 2009-04-17 17:49 木野狐(Neil Chen)      

@夜之悲哀

这里有答案的:
http://blogs.msdn.com/sqlclr/archive/2006/06/22/643551.aspx

需要创建一个 UserDefinedType 封装参数,然后再来一个 UserDefinedFunction 构造和传递这个包含多个参数的对象,然后就可以调用。

我试验成功的代码如下:

use test
go

create table test4
(
id int identity(1,1) primary key,
val int,
weight int
)
go

insert into test4(val, weight)
select 1, 2 union all
select 1, 3 union all
select 3, 2 union all
select 4, 3
go


UserDefinedType:
=======================
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedType(Format.UserDefined, MaxByteSize = 8000)]
public struct TestData : INullable, IBinarySerialize
{
public override string ToString()
{
throw new Exception("The method or operation is not implemented.");
}

public bool IsNull
{
get
{
return false;
}
}

public static TestData Null
{
get
{
throw new Exception("The method or operation is not implemented.");
}
}

public static TestData Parse(SqlString s)
{
throw new Exception("The method or operation is not implemented.");
}

public int Value;
public int Weight;


#region IBinarySerialize Members

void IBinarySerialize.Read(System.IO.BinaryReader r)
{
Value = r.ReadInt32();
Weight = r.ReadInt32();
}


void IBinarySerialize.Write(System.IO.BinaryWriter w)
{
w.Write(Value);
w.Write(Weight);
}
#endregion
}


UserDefinedFunction:
=============================
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction]
public static TestData MakeTestData(int value, int weight)
{
TestData o = new TestData();
o.Value = value;
o.Weight = weight;
return o;
}
};



SqlUserDefinedAggregate:
========================
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(TestData t)
{
sum += (long)t.Value * (long)t.Weight;
count += (int)t.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;
}
}
}


部署后,调用示例代码:
select dbo.WeightedAvg(dbo.MakeTestData(val, weight))
from test4

输出结果为 2.

验证:
select (1*2 + 1*3 + 3*2 + 4*3) / (2+3+2+3)

输出也是 2.

  回复  引用  查看    




发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 561504




相关文章:

相关链接: