sql-server - 无法将空值传递给自定义聚合

标签 sql-server aggregate-functions sqlclr median user-defined-aggregate

下午,

我正在编写一个自定义中值函数(无需查看现有解决方案,我喜欢挑战),经过大量摆弄后,我已经完成了大部分工作。但是,我无法传入包含空值的列。我正在 C# 代码中处理这个问题,但它似乎在到达那里之前就被 SQL 阻止了。

您收到此错误...

Msg 6569, Level 16, State 1, Line 11 'Median' failed because parameter 1 is not allowed to be null.

C#:

 namespace SQLMedianAggregate
{
    [System.Serializable]
    [Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
   Microsoft.SqlServer.Server.Format.UserDefined,
   IsInvariantToDuplicates = false, // duplicates may change results
   IsInvariantToNulls = true,      // receiving a NULL is handled later in code 
   IsInvariantToOrder = true,       // is sorted later
   IsNullIfEmpty = true,            // if no values are given the result is null
        MaxByteSize = -1,
   Name = "Median"                 // name of the aggregate
)]

    public struct Median : IBinarySerialize
    {
        public double Result { get; private set; }

        public bool HasValue { get; private set; }

        public DataTable DT_Values { get; private set; } //only exists for merge essentially

        public static DataTable DT_Final { get; private set; } //Need a static version so its accesible within terminate

        public void Init()
        {
            Result = double.NaN;
            HasValue = false;
            DT_Values = new DataTable();
            DT_Values.Columns.Add("Values", typeof(double));
            DT_Final = new DataTable();
            DT_Final.Columns.Add("Values", typeof(double));
        }

        public void Accumulate(double number)
        {

            if (double.IsNaN(number))
            {
                //skip
            }
            else
            {
                //add to tables
                DataRow NR = DT_Values.NewRow();
                NR[0] = number;
                DT_Values.Rows.Add(NR);
                DataRow NR2 = DT_Final.NewRow();
                NR2[0] = number;
                DT_Final.Rows.Add(NR2);
                HasValue = true;
            }
        }

        public void Merge(Median group)
        {
            // Count the product only if the other group has values
            if (group.HasValue)
            {
                DT_Final.Merge(group.DT_Values);
                //DT_Final = DT_Values;
            }
        }

        public double Terminate()
        {
            if (DT_Final.Rows.Count == 0) //Just to handle roll up so it doesn't crash (doesnt actually work
            {
                DataRow DR = DT_Final.NewRow();
                DR[0] = 0;
                DT_Final.Rows.Add(DR);
            }
            //Sort Results
            DataView DV = DT_Final.DefaultView;
            DV.Sort = "Values asc";
            DataTable DTF = new DataTable();
            DTF = DV.ToTable();

            ////Calculate median and submit result
            double MiddleRow = (DT_Final.Rows.Count -1.0) / 2.0;
            if (MiddleRow % 2 != 0)
            {

                double upper =  (double)(DT_Final.Rows[Convert.ToInt32(Math.Ceiling(MiddleRow))]["Values"]);
                double lower =  (double)(DT_Final.Rows[Convert.ToInt32(Math.Floor(MiddleRow))]["Values"]);
                Result = lower + ((upper - lower) / 2);

            } else
            {
                Result = (double)(DT_Final.Rows[Convert.ToInt32(MiddleRow)]["Values"]);
            }
            return Result;
        }

        public void Read(BinaryReader SerializationReader)
        {
            //Needed to get this working for some reason
        }

        public void Write(BinaryWriter SerializationWriter)
        {
            //Needed to get this working for some reason
        }

    }
}

SQL:

DROP AGGREGATE dbo.Median
DROP ASSEMBLY MedianAggregate
CREATE ASSEMBLY MedianAggregate
AUTHORIZATION dbo
FROM 'C:\Users\#######\Documents\Visual Studio 2017\Projects\SQLMedianAggregate\SQLMedianAggregate\bin\Debug\SQLMedianAggregate.dll'
WITH PERMISSION_SET = UNSAFE;


CREATE AGGREGATE dbo.Median (@number FLOAT) RETURNS FLOAT
EXTERNAL NAME [MedianAggregate]."SQLMedianAggregate.Median";

关于我缺少什么设置或代码的任何想法都可以实现这一点。我几乎只是希望它忽略空值。

顺便说一句,SQL 版本是 SQL2008 R2

最佳答案

问题出在你的数据类型上。您需要使用Sql* SQLCLR 参数、返回值和结果集列的类型。在这种情况下,您需要更改:

Accumulate(double number)

进入:

Accumulate(SqlDouble number)

然后,您访问 double使用 Value 的值所有属性(property)Sql*类型有(即本例中的 number.Value)。

然后,在 Accumulate 的开头方法,您需要检查 NULL使用IsNull属性:

if (number.IsNull)
{
  return;
}

此外,有关使用 SQLCLR 的更多信息,请参阅我在 SQL Server Central 上撰写的有关此主题的系列文章:Stairway to SQLCLR (需要免费注册才能阅读该网站上的内容,但这是值得的:-)。

而且,由于我们在这里讨论中值计算,请参阅我撰写的关于 UDA 和 UDT 主题的文章(也在 SQL Server Central 上),其中使用中值作为示例:Getting The Most Out of SQL Server 2005 UDTs and UDAs 。请记住,本文是针对 SQL Server 2005 编写的,该版本对 UDT 和 UDA 的内存有 8000 字节的硬限制。 SQL Server 2008 中取消了该限制,因此您可以简单地设置 MaxByteSize,而不是使用该文章中所示的压缩技术。在 SqlUserDefinedAggregate-1 (正如您当前所做的那样)或 SqlMetaData.MaxSize (或非常接近的东西)。

此外,DataTable对于这种类型的操作来说有点严厉。您所需要的只是一个简单的 List<Double> :-)。


关于以下代码行(此处分为两行以防止需要滚动):

public static DataTable DT_Final { get; private set; }
   //Need a static version so its accesible within terminate

这是对 UDA 和 UDT 工作原理的巨大误解。请不要在这里使用静态变量。静态变量在 session 之间共享,因此您当前的方法不是线程安全的。因此,您要么会收到有关已声明的错误,要么各种 session 会更改其他 session 不知道的值,因为它们都会共享单个实例 DT_Final 。如果使用并行计划,则错误和/或奇怪的行为(即无法调试的错误结果)可能会在单个 session 中发生。

UDT 和 UDA 被序列化为存储在内存中的二进制值,然后被反序列化,以保持其状态不变。这就是 Read 的原因和Write方法,以及为什么需要让这些方法发挥作用。

再说一次,你不需要(或想要)DataTables在这里,因为它们使操作过于复杂并占用比理想情况更多的内存。请参阅我上面链接的有关 UDA 和 UDT 的文章,了解中值运算(以及一般的 UDA)应如何工作。

关于sql-server - 无法将空值传递给自定义聚合,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45168265/

相关文章:

sql - 无法显示平均销售额,包括没有销售额的地区

mysql - 我在 MySQL 中遇到 SUM() 问题

sql-server-2008-r2 - SQL71501 : Trigger: [dbo]. [TriggerName] 具有对对象 [dbo].[TableName] 的未解析引用

sql-server - VARCHAR 完全像 20 世纪 90 年代吗?

c# - 如何在 Entity Framework 中使用 Date Only 作为数据类型

sql - 在窗口函数中计算运行总和

c# - 如何匹配 UDT T-SQL 和 C# CLR 类型?

c# - 在 SQL Server 2014 中为不安全的程序集创建非对称 key

sql-server - "FirstName"和 "LastName"存储在数据库 USER 或 USER_PROFILE 表中?

sql - 在 SQL Server 和 Oracle 中获取今天日期的兼容语法