之前写过一个合并字符串的CLR聚合函数,基本是照抄MS的示例,外加了一些处理,已经投入使用很长时间,没什么问题也就没怎么研究,近日想改造一下,遇到一些问题,遂捣鼓一番,有些心得,记录如下。

一、杂项

  • CLR聚合函数在SQL中虽然是函数,但在C#中实际上是以一个类或结构的形式存在的(是的,实现聚合函数的实体可以是结构,也可以是类),这点有别于CLR普通函数,后者是类中的一个方法,鉴于此,下文把实现聚合函数的类或结构统称为聚合类,以免读者思维混淆
  • 聚合类必须用特性[SqlUserDefinedAggregate]进行描述。MS示例中还用了[Serializable]特性,经测这个特性不是必须的
  • 聚合类在执行时总是会被序列化和反序列化,这就要求聚合类要满足可序列化,详情在下面有说
  • SqlUserDefinedAggregate特性的IsInvariantToNulls、IsInvariantToDuplicates、IsInvariantToOrder、IsNullIfEmpty这几个属性是给SQL引擎的查询处理器使用的,大概是用作优化执行计划的依据之类,跟聚合类的实现没有关系。什么意思,比如IsInvariantToNulls=true,是告诉SQL查询处理器,我这个聚合函数不管你字段中有没有null(指SQL中的null,下同),返回的结果都是一致的,那么查询处理器可能就会根据这个来确定是否复用已存在的执行计划;它并不是说,会自动帮你过滤掉传入Accumulate方法的null值。换句话,设置IsInvariantToNulls=true后,Accumulate中该进来null的照样会进来,你该处理的还得处理。同样的,其它几个属性也是如此,它们不会帮你负责任何事,该你操的心还得操。所以既然如此,那么你的实现最好与上述属性描述的一致,不然欺骗了查询处理器,估计有好果子给你吃。就好比你跟MM说请她吃6块钱的麻辣烫,实际上你只买了1块钱的,后果自己脑补

二、执行顺序

经打断点调试,聚合类是按如下顺序执行:

  1. 进入Init方法。这是聚合开始后的第一步,试过给类加无参构造函数,但没进去,这里聚合类就已经在暗自向你警告,不要把它当一般class看待。至于为什么,不知道,望高人解答
  2. 进入Accumulate方法(这一步不是必然发生的,稍后说明)。该方法的参数就是最后部署到SQL中后,调用聚合时可传入的参数(SQL 2005只支持1个参数),相当于面向SQL的一个入口,要聚合的元素会逐个逐个被扔进来,进来一个执行一次,完了再进来一个再执行一次,所以该方法会根据要聚合的元素个数循环执行多次,当然,如果元素为0个,就不会执行该方法,会直接跳到下一步,这就是为什么说这一步不是必然发生的。需要说明的是,聚合函数的工作是以分组为一个周期,就是GROUP BY出来有几组,聚合函数就会调用几次,这里说的是每一次中的执行顺序,所以Accumulate方法的循环次数是单组的行数,并不是所有组的行数。举个栗子,GROUP BY出来2组,第1组有2行,第2组有3行,那么整个聚合函数会被调用2次,第一次中Accumulate会循环2次,完了进行下一步,整个周期完成后,再开始聚合下一个组,显然,第二轮中Accumulate会循环3次
  3. 进行序列化。干毛要序列化,我也想知道,我只知道这步之后,聚合类的所有字段的值都会清空(准确说是重置为类型默认值),所以如果不在序列化时抓住机会赶紧保存数据的话,将会使之前在Init和Accumulate中做的工作全部泡汤,因为在下一步的反序列化过程中你将得不到任何数据,进而导致在最终的Terminate方法中将无数据可返回!所以序列化这一步一定要着重理解。这一步只有在自己实现序列化的情况下才看得出来发生过。简单说聚合类的序列化行为分为两种,由SqlUserDefinedAggregate特性的Format属性指定,该属性(是个枚举类型)共有3个值:Unknown、Native、UserDefined,其中Unknown是作为一个缺省值存在,类似其它枚举中的None之类,代表尚未设置,在使用SqlUserDefinedAggregate特性时,Format必须指定为Native或UserDefined,如果是Unknown,则会抛异常。所以聚合类的序列化行为就只能是Native、UserDefined两种:
    • Native。代表聚合类交给CLR去序列化和反序列化,不需要自己实现,看起来很美,但是Native方式有些前提,就是聚合类只能存在值类型的成员,不能有引用类型的成员,包括string,并且如果聚合类是class而不是struct,那必须用[StructLayout(LayoutKind.Sequential)]特性进行标记,如果不满足上述条件,部署必败。所以有时候你不能偷懒,必须用UserDefined方式
    • UserDefined。意味着必须自己实现序列化和反序列化行为,具体是通过让聚合类实现IBinarySerialize接口进行,正如MS示例那样。该接口有两个方法,public void Write(BinaryWriter w)和public void Read(BinaryReader r),分别代表序列化和反序列化过程。终于说到重点了,在聚合类进行到序列化这一步时,你要负责把你想保存的所有数据都写入到w(一个BinaryWriter实例)的基础流中,具体可通过w的Write方法进行,也可以直接访问w.BaseStream操作基础流,或者像new BinaryFormatter().Serialize(w.BaseStream, obj)这样,把整个对象用BinaryFormatter写到基础流中,总之方法多样,属于流的知识范畴,本文不赘述,反正最终目的就是把数据写进w.BaseStream,保险起见,写完以后可以w.Flush()一下。这里就要说到第二个重点了,事关可写入的数据量的问题,SqlUserDefinedAggregate特性有个MaxByteSize属性,当采用UserDefined方式时,必须指定该属性,表示在序列化时最多可以写进多少字节的数据。不指定就是0,就是什么数据也保存不了~玩毛。MaxByteSize可以设置的最大值是由SqlUserDefinedAggregateAttribute.MaxByteSizeValue常量决定的,而这个常量.net2.0-3.5都是8000,后续版本不知道有没有变动。也就是说,序列化时,最多可以写入8000字节的数据,可以保存4000汉字?哎哟不错哦~NONONO,据我调试,w的编码方式是UTF8(不确定跟环境有没有关系,因为w是CLR负责传入的,什么情况下传入什么编码的w,无从考究。如果是固定传UTF8,那只能说有点坑非ASCII区的人民了),且不可更改,也就是1个汉字可能占据3~4个字节,按3字节也就2600个汉字左右,应当说很不富裕,只能求神拜佛应用中触碰不到这个极限。所以我的意见,一定要省着用这个容量,只保存必要的数据,不要图省事把整个整个的对象序列化进去。比如MS的示例就只把StringBuilder中的string塞进去,而没有把整个StringBuilder对象塞进去
  4. 进行反序列化。上回说道,把想保存的数据序列化,到了这一步,自然就是把数据取出来。同样,可以通过r(一个BinaryReader)的各种ReadXXX方法取,也可以访问r.BaseStream操作基础流取出数据。这里头脑要保持清醒,就是取出数据以后是为了在Terminate方法中处理并返回结果,而不是非得把成员对象还原了,然后再去Terminate中操作对象。什么意思,还拿MS的示例说事,不过这次它是反面教材,在Read中得到之前保存的string以后,没必要还原成StringBuilder,完全可以用一个string字段去接住,然后在Terminate处理该字段并返回就好了
  5. 进入Terminate方法。上面说过Accumulate是面向SQL的入口,而Terminate就是出口了,聚合计算的结果就是通过Terminate返回给调用者,所以该方法的返回类型就是在SQL中得到的类型。通过上文,知道在Accumulate和Terminate之间,隔了一个序列化与反序列化的环节,并且要知道在序列化后,类字段的值已经被清空过了,已经不是当初那个类字段了(除非在反序列化时你把它们还原了)。清楚这一点,你就应该知道像这样的看起来天经地义的做法:
    public void Accumulate(SqlString str)
    {
    s = str;
    } public SqlString Terminate()
    {
    return new SqlString(s);
    }

    在聚合类中是极大的错误,除非s在序列化时得到保存并在反序列化时进行还原。

  6. 开始下一组的Init→Accumulate→序列化/反序列化→Terminate。当然如果没有下一组,整个结束

完了?不是还有个Merge方法么,很抱歉,我也不知道这货什么时候才会用到。在我多次调试中,始终没遇到执行Merge的情况。根据MSDN文档所述,我的猜测是,CLR并不保证在一次聚合中都使用同一个聚合类实例,它随时有可能另开一个实例来工作,并利用新开实例的Merge方法将旧实例的数据并入新实例中,完了释放旧实例。不知道这个猜测对不对,撸过高手若清楚,还望指教,谢过先。如果这个猜测没错的话,显然Merge方法要做的就是把旧实例(other)的数据并入当前实例,具体应该怎么写读者应该已心中有数了。要注意的是,如果聚合类是设计为只处理非重复元素的话,那么可以保证在每个实例中存储的元素都是唯一的,但两个实例中的元素却有可能存在相同,在实现Merge时要留意这一点,要确保并入后的数据仍然是唯一的。

三、最后

目前在我看来,聚合类它虽然在C#中是个类/结构,但处处透着古怪,比如没有执行构造函数,运行期间又要清空类字段并转而采用序列化和反序列化的方式传递状态,使它又不那么像一个正常的类,所以我建议在完全弄清楚它之前,不要使用一些OOP的手法去实现它,比如继承重写什么的,想都不要想,老老实实填空就好。另外,对于文中提出的疑惑,希望得到高手指教,再次谢过。

最后附上一枚改造好的字符串聚合(忽略null、空白、重复字串、移除首尾空白):

using Microsoft.SqlServer.Server;
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.IO; namespace AhDung.SqlClr
{
[SqlUserDefinedAggregate(
Format.UserDefined,
IsInvariantToNulls = true,
IsInvariantToDuplicates = true,
IsInvariantToOrder = true,
MaxByteSize = SqlUserDefinedAggregateAttribute.MaxByteSizeValue)
]
public class JoinString : IBinarySerialize
{
string sptr, result;
Dictionary<string, object> dic; public void Init()
{
sptr = string.Empty;
dic = new Dictionary<string, object>(StringComparer.CurrentCultureIgnoreCase);//忽略大小写
} public void Accumulate(SqlString str, SqlString separater)
{
if (sptr.Length == && !separater.IsNull && !string.IsNullOrEmpty(separater.Value)) { sptr = separater.Value; }
string s;
if (str.IsNull || (s = str.Value.Trim()).Length == || dic.ContainsKey(s)) { return; }
dic.Add(s, null);
} public void Merge(JoinString other)
{
foreach (string s in other.dic.Keys)
{
if (dic.ContainsKey(s)) { continue; }
dic.Add(s, null);
}
} public SqlString Terminate()
{
return new SqlString(result);
} public void Read(BinaryReader r)
{
result = r.ReadString();
} public void Write(BinaryWriter w)
{
string[] ss = new string[dic.Count];
dic.Keys.CopyTo(ss, );
w.Write(string.Join(sptr, ss));
}
}
}

- 完 -

【SQL】CLR聚合函数什么鬼的更多相关文章

  1. PCB MS SQL CLR聚合函数(函数作用,调用顺序,调用次数) CLR说明

    用CLR写函数:标量函数,表值函数 很好理解,如果用聚合函数则不是那么好理解了, 这里将CLR函数说明一下,其实关键是对聚合函数说明 用CLR写聚合函数关键点,是要理解CLR与SQL是如何进行数据交互 ...

  2. SQL Server 聚合函数算法优化技巧

    Sql server聚合函数在实际工作中应对各种需求使用的还是很广泛的,对于聚合函数的优化自然也就成为了一个重点,一个程序优化的好不好直接决定了这个程序的声明周期.Sql server聚合函数对一组值 ...

  3. PCB MS CLR 聚合函数 joinString加排序实现

    准备着手,动态列SQL查询,要实现动态列SQL,会运用到聚合函数,而SQL本身聚合函数有限, 无满足此用户任意字段组合变化,再加上工艺流程重复相同工序,如;沉铜1,沉铜2对应工序代码都是相同的 而通常 ...

  4. 数据库开发基础-SQl Server 聚合函数、数学函数、字符串函数、时间日期函数

    SQL 拥有很多可用于计数和计算的内建函数. 函数的语法 内建 SQL 函数的语法是: SELECT function(列) FROM 表 函数的类型 在 SQL 中,基本的函数类型和种类有若干种.函 ...

  5. SQL serve 聚合函数、字符串函数

    1.聚合函数 sum,avg,max,min,count        having后面只能跟聚合函数 2.数学函数和字符串函数 3.练习: 1)新建一个学生信息表,根据问题写出程序. 2)新建一个超 ...

  6. sql server聚合函数sum计算出来为空,怎样返回0

    通常我们计算数据库中表的数据有几个常用的聚合函数 1.count : 计数 2.sum: 计算总和 3.avg: 取平均值 4.max: 取最大值 5.min: 取最小值 6.isnull: 当返回数 ...

  7. SQL Server聚合函数

    聚合函数对一组值计算后返回单个值.除了count(统计项数)函数以外,其他的聚合函数在计算式都会忽略空值(null).所有的聚合函数均为确定性函数.即任何时候使用一组相同的输入值调用聚合函数执行后的返 ...

  8. SQL Server聚合函数与聚合开窗函数 (转载)

    以下面这个表的数据作为示例. 什么是聚合函数?聚合函数:聚合函数就是对一组值进行计算后返回单个值(即分组).聚合函数在计算时都会忽略空值(null).所有的聚合函数均为确定性函数.即任何时候使用一组相 ...

  9. SQL Server聚合函数与聚合开窗函数

    以下面这个表的数据作为示例. 什么是聚合函数? 聚合函数:聚合函数就是对一组值进行计算后返回单个值(即分组).聚合函数在计算时都会忽略空值(null). 所有的聚合函数均为确定性函数.即任何时候使用一 ...

随机推荐

  1. [Beautifulzzzz的博客目录] 快速索引点这儿O(∩_∩)O~~,红色标记的是不错的(⊙o⊙)哦~

    3D相关开发 [direct-X] 1.direct-X最小框架 [OpenGL] 1.环境搭建及最小系统 [OpenGL] 2.企业版VC6.0自带的Win32-OpenGL工程浅析 51单片机 [ ...

  2. [我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之摄像机介绍Cameras

    [我给Unity官方视频教程做中文字幕]beginner Graphics – Lessons系列之摄像机介绍Cameras 最近得到一些Unity官方视频教程,一看全是纯英文的讲解,没有任何字幕或者 ...

  3. MVVM架构~knockoutjs系列之验证成功提示显示

    返回目录 对于knockout.validation来说,我们已经知道了如何去验证大部分表单元素,而有时,我们的需求希望在每个元素验证成功后,去显示正确的提示,这个我们很容易的使用self.元素.is ...

  4. Atitit 编程语言知识点tech tree v2 attilax大总结

    Atitit 编程语言知识点tech tree v2 attilax大总结 大分类中分类小分类知识点原理与规范具体实现(javac#里面的实现phpjsdsl(自己实现其他语言实现 类与对象实现对象实 ...

  5. JS的脚本语言

    js的脚本语言全程javascript在网页里面使用的脚本语言:分类:1.嵌入网页里面2.在外部脚本标签可以写在网页的任何地方,但一般都写在网页的底部:<script type="te ...

  6. iOS ---Swift学习与复习

    swift中文网 http://www.swiftv.cn http://swifter.tips/ http://objccn.io/ http://www.swiftmi.com/code4swi ...

  7. MVC及WebAPI添加Jsonp支持

    Windows Live Writer 有点问题,着色代码看起来不清晰,所以贴的图片,完整代码在最后. 1:MVC实现 大致思路就是实现一个JsonpResult,在ExecuteResult内实现支 ...

  8. writing-mode改变文字书写方式

    古代书写方式都是垂直方向上的,如果要实现这种效果的话,还是挺麻烦的,不过现在CSS3有一个"writing-mode"属性,它可以改变文字的书写方式. writing-mode:h ...

  9. Shader LOD

    设置:单个设置Shader.maximumLOD.全局设置Shader.globalMaximumLOD.QualitySettings里面的Maximum LODLevel 原理:小于指定值的sha ...

  10. sys.dm_db_wait_stats

    sys.dm_db_wait_stats 返回在操作期间执行的线程所遇到的所有等待的相关信息. 可以使用此聚合视图来诊断 Azure SQL Database 以及特定查询和批处理的性能问题. 执行查 ...