【问题标题】:Cannot Pass Null Value to Custom Aggregate无法将空值传递给自定义聚合
【发布时间】:2017-12-23 09:49:19
【问题描述】:

下午,

我正在编写一个自定义的中值函数(不​​看现有的解决方案,我喜欢挑战),经过大量的摆弄,我大部分时间都在那里。但是,我不能传入包含空值的列。我在 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 btw

【问题讨论】:

  • 为什么你需要一个自定义的中位数聚合。这是平均值的另一个名称,平均值有一个内置函数,它忽略 NULL 值。 docs.microsoft.com/en-us/sql/t-sql/functions/avg-transact-sql
  • @SeanLange 我认为原始帖子是指计算中位数。您指的内置函数是否计算平均值?有关不同类型平均值的说明,请参阅此链接 - bbc.co.uk/bitesize/ks2/maths/data/mode_median_mean_range/read/1
  • 感谢 Sean @the_doc 的澄清!链接完美地描述了我所追求的。中间值,而不是所有值的平均值。
  • 您可以在 t-sql 中轻松完成所有这些工作。这是一个包含一些示例的链接。 blogs.lessthandot.com/index.php/datamgmt/datadesign/…
  • 嗨@SeanLange 不幸的是,这种方法不支持按另一列分组。另外,我们谈论的是相当大的数据集和查询,我认为聚合将是最有效的方法,并且不会让 BI 开发人员感到头疼,这个想法是用这个中值聚合替换所有 AVG() 函数以获得更具代表性报告。

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


【解决方案1】:

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

Accumulate(double number)

进入:

Accumulate(SqlDouble number)

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

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

if (number.IsNull)
{
  return;
}

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

而且,由于我们在这里讨论的是中值计算,请参阅我写的文章(也在 SQL Server Central 上),以 UDA 和 UDT 为主题,使用中值作为示例:Getting The Most Out of SQL Server 2005 UDTs and UDAs。请记住,这篇文章是为 SQL Server 2005 编写的,它对 UDT 和 UDA 的内存有 8000 字节的硬限制。 SQL Server 2008 中取消了该限制,因此您可以简单地将SqlUserDefinedAggregate 中的MaxByteSize 设置为-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 之间共享,因此您当前的方法是不是线程安全的。所以你要么得到关于它已经被声明的错误,要么各种会话会改变其他会话不知道的值,因为它们都会共享单个实例 DT_Final。如果使用并行计划,错误和/或奇怪的行为(即您无法调试的错误结果)可能会在单个会话中发生。

UDT 和 UDA 被序列化为存储在内存中的二进制值,然后被反序列化以保持其状态不变。这就是 ReadWrite 方法的原因,也是您需要让它们工作的原因。

同样,您不需要(或不想要)DataTables,因为它们会使操作过于复杂,并且占用的内存比理想的要多。请参阅我上面链接的关于 UDA 和 UDT 的文章,了解 Median 操作(以及一般的 UDA)应该如何工作。

【讨论】:

  • SQLDouble 做了一些其他的调整谢谢!我认为数据表有点矫枉过正,我将其更改为列表 :-) 我通过电子邮件新闻信得知你是楼梯系列,非常有趣的东西,大粉丝,我应该等几天开始这个并得到更好地理解它。我仍然不确定为什么必须静态声明该对象,以便在它到达终止()时将其处理掉,我将在病房上进行实验!再次感谢!
  • 欢迎您。我刚刚更新了我对静态变量的担忧的答案。它既不需要也不需要。此处不能使用静态变量。并感谢那些关于享受我的楼梯系列的客气话:-)。
  • 感谢 Solomon,我从您的文章中看到 binaryreader / binarywriter 是使对象永久存在的原因,因此它仍然可以在 terminate() 中访问。我已经根据您的所有建议进行了调整,并且按预期工作!除了 WITH ROLLUP 不起作用,应该吗?我得到索引超出范围异常。
  • @Ollie 是的,WITH ROLLUP 应该可以工作。我使用我通过SELECT so.[schema_id], AVG(CONVERT(BIGINT, so.[object_id])), SQL#.Agg_Median(CONVERT(BIGINT, so.[object_id])) FROM sys.objects so GROUP BY so.[schema_id] WITH ROLLUP; 编写的SQL# 库的免费版本进行了测试,结果很好。鉴于错误消息,很可能是您的 Merge 方法存在问题,因为聚合是并行使用的,并且该方法将它们重新组合在一起。我怀疑你没有正确初始化一些东西。
  • 好的,谢谢你,我现在回家了,我会在明天回到我的电脑时调试/重新编码,然后告诉你。 :-)
猜你喜欢
  • 1970-01-01
  • 2020-09-26
  • 1970-01-01
  • 1970-01-01
  • 2021-09-11
  • 1970-01-01
  • 1970-01-01
  • 2018-04-04
  • 2013-06-08
相关资源
最近更新 更多