【SQL】CLR聚合函数什么鬼

之前写过一个合并字符串的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 == 0 && !separater.IsNull && !string.IsNullOrEmpty(separater.Value)) { sptr = separater.Value; }
            string s;
            if (str.IsNull || str.Value.Trim().Length == 0 || dic.ContainsKey(s = str.Value.Trim())) { 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, 0);
            w.Write(string.Join(sptr, ss));
        }
    }
}

- 完 -

时间: 2024-10-09 08:52:42

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

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

Sql server聚合函数在实际工作中应对各种需求使用的还是很广泛的,对于聚合函数的优化自然也就成为了一个重点,一个程序优化的好不好直接决定了这个程序的声明周期.Sql server聚合函数对一组值执行计算并返回单一的值.聚合函数对一组值执行计算,并返回单个值.除了 COUNT 以外,聚合函数都会忽略空值. 聚合函数经常与 SELECT 语句的 GROUP BY 子句一起使用. v1.写在前面 如果有对Sql server聚合函数不熟或者忘记了的可以看我之前的一片博客.sql server 基

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

SQL 拥有很多可用于计数和计算的内建函数. 函数的语法 内建 SQL 函数的语法是: SELECT function(列) FROM 表 函数的类型 在 SQL 中,基本的函数类型和种类有若干种.函数的基本类型是: Aggregate 函数 Scalar 函数 合计函数(Aggregate functions) Aggregate 函数的操作面向一系列的值,并返回一个单一的值. 注释:如果在 SELECT 语句的项目列表中的众多其它表达式中使用 SELECT 语句,则这个 SELECT 必须使

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

通常我们计算数据库中表的数据有几个常用的聚合函数 1.count : 计数 2.sum: 计算总和 3.avg: 取平均值 4.max: 取最大值 5.min: 取最小值 6.isnull: 当返回数据为空,默认设置为0 7.coalesce: 当返回数据为空,默认设置为0 1.count的使用 下面是一个student表的所有数据 1) 计算表中数据的数量,count(*),返回结结果是6 2)计数表中地址数据的数量,count(address),返回结果是5 2.sum的使用 1)计算总的年

SQL Server聚合函数

聚合函数对一组值计算后返回单个值.除了count(统计项数)函数以外,其他的聚合函数在计算式都会忽略空值(null).所有的聚合函数均为确定性函数.即任何时候使用一组相同的输入值调用聚合函数执行后的返回值都是相同的,无二义性.T-SQL提供的聚合函数一共有13个之多. 聚合函数通常会在下列场合使用: 1.select语句的选择列表,包括子查询和外部查询. 2.使用compute或compute by产生汇总列时. 3.having子句对分组的数据记录进行条件筛选. 聚合函数 1.平均值AVG A

SQL server聚合函数、数学函数、字符串函数

一.基础语句 二.数学函数与字符串函数 三.练习 1.创建一个学生信息表,根据要求写出程序 2.新建一个超市表,进了十种商品,个数都是十件

转载——CLR标量函数、表值函数和聚合函数(UDA)

本节主要介绍使用CLR创建标量函数,表值函数和聚合函数. 所谓标量函数指的就是此函数只返回一个值.表值函数返回值是一个表.聚合函数是在select语句中使用的,用来聚合一个结果集,类似于Sum()或是Count()等内置的函数,而且真正的自定义聚合函数目前只能用CLR来实现. 下面的例子使用了SQLServer自带的pubs数据库. 1.CLR标量函数 1.1无参函数     ///     /// 标量函数,不带参数     ///     ///     [Microsoft.SqlServ

sql server 2012 自定义聚合函数(MAX_O3_8HOUR_ND) 计算最大的臭氧8小时滑动平均值

采用c#开发dll,并添加到sql server 中. 具体代码,可以用visual studio的向导生成模板. using System; using System.Collections; using System.Data; using Microsoft.SqlServer.Server; using System.Data.SqlTypes; using System.IO; using System.Text; [Serializable] [Microsoft.SqlServer

C#写的SQL聚合函数

SQL Server 字符串连接聚合函数. 注册程序集: 拷贝“SqlStrConcate.dll”至<sql安装根目录>/MSSQL.1/MSSQL/Binn目录下,执行下面的SQL: CREATE ASSEMBLY [SqlStrConcate] AUTHORIZATION [dbo] FROM 'D:/Program Files/Microsoft SQL Server/MSSQL.1/MSSQL/Binn/SqlStrConcate.dll' WITH PERMISSION_SET =

SQL Server 自定义聚合函数

说明:本文依据网络转载整理而成,因为时间关系,其中原理暂时并未深入研究,只是整理备份留个记录而已. 目标:在SQL Server中自定义聚合函数,在Group BY语句中 ,不是单纯的SUM和MAX等运算,可以加入拼接字符串. 环境: 1:Sqlserver 2008 R2 2:Visual Studio 2013 第一部分: .net代码: using System; using System.Data; using Microsoft.SqlServer.Server; using Syst