【发布时间】:2015-11-28 10:15:12
【问题描述】:
我正在尝试创建一个 SQL CLR 函数,该函数执行类似于此处解释的运行总计:SQL Server and fastest running totals using CLR – Updated
但是,此函数仅对列中的所有值进行汇总。我要做的是重置运行总计,以便在 field1(SQL varchar)和 field2(SQL 数字)之间发生转换时它变为 0。但是,我似乎无法弄清楚执行此操作的必要代码。我尝试使用 CallContext 存储这两个字段,但我一直遇到空引用。我认为转换检测的工作方式与运行总计计算类似,但事实并非如此。我已经为此绞尽脑汁好几个小时了。
这是我使用这个函数的表,主键是REF_NO:
CREATE TABLE [dbo].[USER_TB_TIME_TICKETS] (
[REF_NO] [dbo].[T_DOC_NO] NOT NULL,
[REF_DAT] [dbo].[T_DAT_SMALL] NOT NULL,
[ACT_DAT] [dbo].[T_DAT_SMALL] NOT NULL,
[USR_ID] [dbo].[T_USR_ID] NOT NULL,
[CUST_NO] [dbo].[T_CUST_NO] NOT NULL,
[JOB_NO] [dbo].[USER_TB_T_JOB_NO] NOT NULL,
[CODE] [dbo].[USER_TB_T_SERV_COD] NOT NULL,
[HRS_WORKED] [dbo].[USER_TB_T_HOURS] NOT NULL,
[DESCR] [varchar](200) NOT NULL,
[MILEAGE] [dbo].[USER_TB_T_MILES] NULL,
[NOTES] [dbo].[USER_TB_T_NOTE] NULL,
[BILL_FLG] [varchar](1) NULL,
[BILL_HRS] [decimal](6, 2) NULL,
[EXCESS_HRS] [decimal](6, 2) NULL,
)
这是 SQL 标量 CLR 函数包装器:
CREATE FUNCTION [dbo].[fn_RunningTotalDecimal_15_2_ResetStringNumeric](
@val [decimal](15, 2),
@id [tinyint],
@rowNo [int],
@nullValue [decimal](15, 2),
@field1 [nvarchar](15),
@field2 [numeric](18, 0))
RETURNS [decimal](15, 2) WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [SqlClrRunningTotals].[RDSAPI.SQLClrRunningTotals.RunningTotals].[RunningTotalDecimalResetStringNumeric]
这是 C# 代码:
/// <summary>
/// Storage Structure for holding actual Total and row number for security check.
/// </summary>
/// <typeparam name="T">Totals Data Type</typeparam>
private struct RtStorage<T> where T : struct
{
public T Total;
public int RowNo;
}
private struct StringFieldStorage<T> where T : struct
{
public T stringField;
}
private struct NumericFieldStorage<T> where T : struct
{
public T numericField;
}
....Other extraneous class code...
/// <summary>
/// Calculates a running totals on Decimal data type based on transistion between a string and numeric field.
/// </summary>
/// <param name="val">Value of current row</param>
/// <param name="id">ID of the function in single query</param>
/// <param name="rowNo">Specifies expecter rowNo. It is for security check to ensure correctness of running totals</param>
/// <param name="nullValue">Value to be used for NULL values</param>
/// <param name="field1">String field</param>
/// <param name="field2">Numeric field</param>
/// <returns>SqlDecimal representing running total</returns>
[SqlFunction(IsDeterministic = true)]
public static SqlDecimal RunningTotalDecimalResetStringNumeric(SqlDecimal val, SqlByte id, int rowNo, SqlDecimal nullValue, SqlString field1, SqlDecimal field2)
{
string dataName = string.Format("MultiSqlRt_{0}", id.IsNull ? 0 : id.Value);
string field1Name = string.Format("MultiSqlField1_{0}", id.IsNull ? 0 : id.Value);
string field2Name = string.Format("MultiSqlField2_{0}", id.IsNull ? 0 : id.Value);
object lastSum = CallContext.GetData(dataName);
object field1Value = CallContext.GetData(field1Name);
object field2Value = CallContext.GetData(field2Name);
var storage = lastSum != null ? (RtStorage<SqlDecimal>)lastSum : new RtStorage<SqlDecimal>();
storage.RowNo++;
var stringFieldStorage = field1Value != null ? (StringFieldStorage<SqlString>)field1Value : new StringFieldStorage<SqlString>();
var numericFieldStorage = field2Value != null ? (NumericFieldStorage<SqlDecimal>)field2Value : new NumericFieldStorage<SqlDecimal>();
if (storage.RowNo != rowNo)
throw new System.InvalidOperationException(string.Format("Rows were processed out of expected order. Expected RowNo: {0}, received RowNo: {1}", storage.RowNo, rowNo));
if (stringFieldStorage.stringField != field1 || (stringFieldStorage.stringField == field1 && numericFieldStorage.numericField != field2))
{
storage.Total = new SqlDecimal(0);
stringFieldStorage.stringField = field1;
numericFieldStorage.numericField = field2;
}
if (!val.IsNull)
storage.Total = storage.Total.IsNull ? val : storage.Total + val;
else
storage.Total = storage.Total.IsNull ? nullValue : (nullValue.IsNull ? storage.Total : storage.Total + nullValue);
CallContext.SetData(dataName, storage);
CallContext.SetData(field1Name, stringFieldStorage);
CallContext.SetData(field2Name, numericFieldStorage);
return storage.Total;
}
编辑
这里有一张图片让它更清楚一点。 (感兴趣的列被盯着看。我会放一张图片,但我没有足够的代表)。
CUST_NO,JOB_NO,USR_ID,REF_NO,REF_DAT,ACT_DAT,HRS_WORKED,HOURS_QUOTED,BILL_FLG,BILL_HRS,EXCESS_HRS,running_total ATA,1,AML,152364,2015-06-18,2015-06-16,0.25,12.00,Y,0.25,0.00,9.50 ATA,1,AMA,152367,2015-06-18,2015-06-18,0.25,12.00,Y,0.25,0.00,9.75 ATA,1,AML,152372,2015-06-18,2015-06-18,1.50,12.00,Y,1.50,0.00,11.25 ATA,1,AMA,152569,2015-06-22,2015-06-22,0.50,12.00,Y,0.50,0.00,11.75 ATA,1,AMA,152735,2015-06-25,2015-06-25,0.50,12.00,Y,0.25,0.25,12.25 **ATA**,**1**,AMA,153472,2015-07-14,2015-07-13,0.25,12.00,N,0.00,0.25,**12.50** **ATA**,**2**,SCP,152097,2015-06-12,2015-06-10,0.50,3.00,Y,0.50,0.00,**13.00** ATA,2,CTK,151923,2015-06-11,2015-06-11,0.75,3.00,Y,0.75,0.00,13.75 ATA,2,CTK,151998,2015-06-12,2015-06-12,0.75,3.00,Y,0.75,0.00,14.50我要做的是更改 C# 代码,以便我可以检测到 CUST_NO 或 JOB_NO 之间的更改(如果 CUST_NO 相同),并在转换时将运行总计列重置为 0。本质上,我想对每个 CUST_NO 的每个 JOB_NO 进行运行总计。我意识到这是一个分组函数,但我正在使用 CLR 函数,因为在 SQL 中有效地计算运行总数是很困难的,我将为一个每天都在增长的具有 140,000 多条记录的表执行此操作。由于我使用的是 SQL Server 2008 R2,因此我在 2012 年没有获得 Window ROWS 功能,因此我可以大幅降低读数以运行有关此数据的报告的唯一方法是使用 CLR 函数。性能测试在这篇文章中解释:Best approaches for running totals – updated for SQL Server 2012
另一个编辑
我也愿意接受其他实现的建议。我目前确实有一个游标实现,但同样,它很慢。我已经计时它仍然执行超过 30 秒。
【问题讨论】:
-
“字段 1 和字段 2 之间的转换”。我不知道你那是什么意思。您能否提供一些示例数据(最好是您的预期运行总数的附加列)?
-
是的,我今晚或明天会去。
-
听起来你正在尝试做某种
GROUP BY行为,但我并不完全确定。举个例子会很有帮助。
标签: c# sql .net sql-server sql-server-2008