SQLServer字符串聚合解决方法(CLR)

开发环境:SQL Server2008 R2

写个综合视图,遇到个情况,需要对字符串进行聚合统计,简化如下:

任务号提交人完工数周转车号
X01张三300V001
X01李四200V002
X02王五600V003
X02马六400V004
X02赵七100V005

目的是:需要列出统计任务的完成信息如下:

任务号提交人完工数周转车号
X01张三,李四500V001,V002
X02王五,马六,赵七1100V003,V004,V005

完工数量可以直接sum 后 group by,但是提交人 和 周转车 字符串字段就很麻烦了。google了下,有以下三种办法:

  • ** 自定义聚合函数 ** 如何在sql server的group by语句中聚合字符串字段
    这种方法的思路就是用sql自定义个function,聚合的时候调用。这个办法最大的问题就是在函数中需要把要调用的表名写死,像上面这个需求,就要定义两个函数,一个是对提交人的聚合,一个是对周转车的聚合,而且这里的识别id只有一个,就是任务id(这个是简化需求),我的实际需求是要对任务ID+工序ID作为子件的,这样的函数条件也不好扩展。–所以放弃这个办法。
  • ** 用stuff和for xml path子查询 ** SQL SERVER 2005 中使用for xml path(”)和stuff合并显示多行数据到一行中
    这个方法也可行,但是问题也和1一样,要大段大段的写SQL子查询,而且无法复用,多的话实在受不了。
  • ** 目前找到的以为最好的方法:配合c#自定义聚合函数 ** 源出处:C#实现SQL Server2005的扩展聚合函数
    该方法实现后,调用的SQL就是:
SELECT taskid,SUM(qty),
dbo.StrJoin(workerName,',') as workers, dbo.StrJoin(cartNo,',') as Carts 
FROM taskExecs  GROUP BY taskid

是不是很简单?而且以后出现类似的拼接字符串聚合就都直接调用就好了,一副一劳永逸的姿态。
我对原文的方法做了一些小调整和改变,具体实现如下:

  1. Visual Studio 2015,新建个项目–》模板选SQL Server 数据库项目,命名项目sqlUtil
  2. 新建项–》 SQL CLR c# ==>SQL CLR c# 聚合 ,是个类,命名StrJoin.cs
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Text;、
[Serializable]
[SqlUserDefinedAggregate(
  Format.UserDefined, //use custom 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 struct StrJoin : IBinarySerialize
{
  private StringBuilder sbIntermediate;
  public void Init()
  {
    sbIntermediate = new StringBuilder();
  }
  public void Accumulate(SqlString Value,SqlString contChar)
  {
    if (Value == null || Value.ToString().ToLower().Equals("null"))
    {
      return;
    }
    else
    {
      sbIntermediate.Append(Value).Append(contChar);
    }
  }
  public void Merge(StrJoin Group)
  {
    sbIntermediate.Append(Group.sbIntermediate);
  }
  public SqlString Terminate()
  {
    string output = String.Empty;
    if (sbIntermediate != null && sbIntermediate.Length>0)
    {
      output = sbIntermediate.ToString(0, sbIntermediate.Length - 1);
    }
    return new SqlString(output);
  }
  // This is a place-holder member field
  #region IBinarySerialize Members
  public void Read(System.IO.BinaryReader r)
  {
    sbIntermediate = new StringBuilder(r.ReadString());
  }
  public void Write(System.IO.BinaryWriter w)
  {
    w.Write(this.sbIntermediate.ToString());
  }
  #endregion
}

说明:看上去一脸蒙逼很复杂的样子,其实以上函数有效的部分很简单,重点部分就是

  1. 在Accumulate函数中:传入参数,把字符串拼起来。
  2. 在Terminate函数中: 去掉最后一个连接符并输出。
    主要看这两个动作,就知道了。
    在sqlserver中执行如下:
--打开SQLSERVER的CLR功能
EXEC sp_configure 'clr enabled', 1
RECONFIGURE WITH OVERRIDE
GO

--注册DLL
CREATE ASSEMBLY sqlUtil FROM 'C:\sqlUtil.dll'      --生成的DLL路径
GO

--注册函数
CREATE AGGREGATE [dbo].[StrJoin] (@Value [nvarchar](MAX), 
  @contChar [nvarchar](2))
  RETURNS [nvarchar](MAX)
  EXTERNAL NAME [sqlUtil].[StrJoin]

这样后,就可以愉快的使用了。

如果要更新dll,需要先drop,在create

顺序是 删除引用的函数–>删除dll

DROP AGGREGATE StrJoin
DROP  ASSEMBLY sqlUtil

PS:在这个过程遇到个纠结的问题,就是虚拟机和远程机之间复制文件的时候,居然会有问题,导致一个更新的dll一直是旧版本,而我却以为代码有错。。。。最后用.Net Refector去看dll的函数,才惊觉这个问题,吐血中…. 最后还是用共享传的文件。
*** PS2:据说MYSQL和Oracle其实都有现成的group_contact 和 wm_concat,所以到了SQLSERVER2012,据说也支持了字符串聚合的函数。但是在使用2012之前,等于是用第三种方法提前体验了而已。

    原文作者:meteorfall
    原文地址: https://www.jianshu.com/p/f765f4d96948
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞