Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Table of Contents CLR Scalar-Valued Functions CLR Table-Valued Functions CLR User-Defined Aggregate - Invoking Functions CLR User-Defined Aggregates - Requirements CLR User-Defined Aggregates CLR User-Defined Functions CLR Scalar-Valued Functions 3/24/2017 • 4 min to read • Edit Online A scalar-valued function (SVF) returns a single value, such as a string, integer, or bit value.You can create scalarvalued user-defined functions in managed code using any .NET Framework programming language. These functions are accessible to Transact-SQL or other managed code. For information about the advantages of CLR integration and choosing between managed code and Transact-SQL, see Overview of CLR Integration. Requirements for CLR Scalar-Valued Functions .NET Framework SVFs are implemented as methods on a class in a .NET Framework assembly. The input parameters and the type returned from a SVF can be any of the scalar data types supported by SQL Server, except varchar, char, rowversion, text, ntext, image, timestamp, table, or cursor. SVFs must ensure a match between the SQL Server data type and the return data type of the implementation method. For more information about type conversions, see Mapping CLR Parameter Data. When implementing a .NET Framework SVF in a .NET Framework language, the SqlFunction custom attribute can be specified to include additional information about the function. The SqlFunction attribute indicates whether or not the function accesses or modifies data, if it is deterministic, and if the function involves floating point operations. Scalar-valued user-defined functions may be deterministic or non-deterministic. A deterministic function always returns the same result when it is called with a specific set of input parameters. A non-deterministic function may return different results when it is called with a specific set of input parameters. NOTE Do not mark a function as deterministic if the function does not always produces the same output values, given the same input values and the same database state. Marking a function as deterministic, when the function isn't truly deterministic can result in corrupted indexed views and computed columns. You mark a function as deterministic by setting the IsDeterministic property to true. Table -Valued Parameters Table-valued parameters (TVPs), user-defined table types that are passed into a procedure or function, provide an efficient way to pass multiple rows of data to the server. TVPs provide similar functionality to parameter arrays, but offer greater flexibility and closer integration with Transact-SQL. They also provide the potential for better performance. TVPs also help reduce the number of round trips to the server. Instead of sending multiple requests to the server, such as with a list of scalar parameters, data can be sent to the server as a TVP. A user-defined table type cannot be passed as a table-valued parameter to, or be returned from, a managed stored procedure or function executing in the SQL Server process. For more information about TVPs, see Use Table-Valued Parameters (Database Engine). Example of a CLR Scalar-Valued Function Here is a simple SVF that accesses data and returns an integer value: using Microsoft.SqlServer.Server; using System.Data.SqlClient; public class T { [SqlFunction(DataAccess = DataAccessKind.Read)] public static int ReturnOrderCount() { using (SqlConnection conn = new SqlConnection("context connection=true")) { conn.Open(); SqlCommand cmd = new SqlCommand( "SELECT COUNT(*) AS 'Order Count' FROM SalesOrderHeader", conn); return (int)cmd.ExecuteScalar(); } } } Imports Microsoft.SqlServer.Server Imports System.Data.SqlClient Public Class T <SqlFunction(DataAccess:=DataAccessKind.Read)> _ Public Shared Function ReturnOrderCount() As Integer Using conn As New SqlConnection("context connection=true") conn.Open() Dim cmd As New SqlCommand("SELECT COUNT(*) AS 'Order Count' FROM SalesOrderHeader", conn) Return CType(cmd.ExecuteScalar(), Integer) End Using End Function End Class The first line of code references Microsoft.SqlServer.Server to access attributes and System.Data.SqlClient to access the ADO.NET namespace. (This namespace contains SqlClient, the .NET Framework Data Provider for SQL Server.) Next, the function receives the SqlFunction custom attribute, which is found in the Microsoft.SqlServer.Server namespace. The custom attribute indicates whether or not the user-defined function (UDF) uses the in-process provider to read data in the server. SQL Server does not allow UDFs to update, insert, or delete data. SQL Server can optimize execution of a UDF that does not use the in-process provider. This is indicated by setting DataAccessKind to DataAccessKind.None. On the next line, the target method is a public static (shared in Visual Basic .NET). The SqlContext class, located in the Microsoft.SqlServer.Server namespace, can then access a SqlCommand object with a connection to the SQL Server instance that is already set up. Although not used here, the current transaction context is also available through the System.Transactions application programming interface (API). Most of the lines of code in the function body should look familiar to developers who have written client applications that use the types found in the System.Data.SqlClient namespace. [C#] using(SqlConnection conn = new SqlConnection("context connection=true")) { conn.Open(); SqlCommand cmd = new SqlCommand( "SELECT COUNT(*) AS 'Order Count' FROM SalesOrderHeader", conn); return (int) cmd.ExecuteScalar(); } [Visual Basic] Using conn As New SqlConnection("context connection=true") conn.Open() Dim cmd As New SqlCommand( _ "SELECT COUNT(*) AS 'Order Count' FROM SalesOrderHeader", conn) Return CType(cmd.ExecuteScalar(), Integer) End Using The appropriate command text is specified by initializing the SqlCommand object. The previous example counts the number of rows in table SalesOrderHeader. Next, the ExecuteScalar method of the cmd object is called. This returns a value of type int based on the query. Finally, the order count is returned to the caller. If this code is saved in a file called FirstUdf.cs, it could be compiled into as assembly as follows: [C#] csc.exe /t:library /out:FirstUdf.dll FirstUdf.cs [Visual Basic] vbc.exe /t:library /out:FirstUdf.dll FirstUdf.vb NOTE /t:library indicates that a library, rather than an executable, should be produced. Executables cannot be registered in SQL Server. NOTE Visual C++ database objects compiled with /clr:pure are not supported for execution on SQL Server. For example, such database objects include scalar-valued functions. The Transact-SQL query and a sample invocation to register the assembly and UDF are: CREATE ASSEMBLY FirstUdf FROM 'FirstUdf.dll'; GO CREATE FUNCTION CountSalesOrderHeader() RETURNS INT AS EXTERNAL NAME FirstUdf.T.ReturnOrderCount; GO SELECT dbo.CountSalesOrderHeader(); GO Note that the function name as exposed in Transact-SQL does not need to match the name of the target public static method. See Also Mapping CLR Parameter Data Overview of CLR Integration Custom Attributes User-Defined Functions Data Access from CLR Database Objects CLR Table-Valued Functions 3/24/2017 • 9 min to read • Edit Online A table-valued function is a user-defined function that returns a table. Beginning with SQL Server 2005, SQL Server extends the functionality of table-valued functions by allowing you to define a table-valued function in any managed language. Data is returned from a table-valued function through an IEnumerable or IEnumerator object. NOTE For table-valued functions, the columns of the return table type cannot include timestamp columns or non-Unicode string data type columns (such as char, varchar, and text). The NOT NULL constraint is not supported. For more information on CLR Table-Valued functions, check out MSSQLTips' Introduction to SQL Server CLR table valued functions! Differences Between Transact-SQL and CLR Table-Valued Functions Transact-SQL table-valued functions materialize the results of calling the function into an intermediate table. Since they use an intermediate table, they can support constraints and unique indexes over the results. These features can be extremely useful when large results are returned. In contrast, CLR table-valued functions represent a streaming alternative. There is no requirement that the entire set of results be materialized in a single table. The IEnumerable object returned by the managed function is directly called by the execution plan of the query that calls the table-valued function, and the results are consumed in an incremental manner. This streaming model ensures that results can be consumed immediately after the first row is available, instead of waiting for the entire table to be populated. It is also a better alternative if you have very large numbers of rows returned, because they do not have to be materialized in memory as a whole. For example, a managed table-valued function could be used to parse a text file and return each line as a row. Implementing Table-Valued Functions Implement table-valued functions as methods on a class in a Microsoft .NET Framework assembly. Your tablevalued function code must implement the IEnumerable interface. The IEnumerable interface is defined in the .NET Framework. Types representing arrays and collections in the .NET Framework already implement the IEnumerable interface. This makes it easy for writing table-valued functions that convert a collection or an array into a result set. Table-Valued Parameters Table-valued parameters are user-defined table types that are passed into a procedure or function and provide an efficient way to pass multiple rows of data to the server. Table-valued parameters provide similar functionality to parameter arrays, but offer greater flexibility and closer integration with Transact-SQL. They also provide the potential for better performance. Table-valued parameters also help reduce the number of round trips to the server. Instead of sending multiple requests to the server, such as with a list of scalar parameters, data can be sent to the server as a table-valued parameter. A user-defined table type cannot be passed as a table-valued parameter to, or be returned from, a managed stored procedure or function executing in the SQL Server process. For more information about table-valued parameters, see Use Table-Valued Parameters (Database Engine). Output Parameters and Table-Valued Functions Information may be returned from table-valued functions using output parameters. The corresponding parameter in the implementation code table-valued function should use a pass-by-reference parameter as the argument. Note that Visual Basic does not support output parameters in the same way that Visual C# does. You must specifiy the parameter by reference and apply the <Out()> attribute to represent an output parameter, as in the following: Imports System.Runtime.InteropServices … Public Shared Sub FillRow ( <Out()> ByRef value As SqlInt32) Defining a Table -Valued Function in Transact-SQL The syntax for defining a CLR table-valued function is similar to that of a Transact-SQL table-valued function, with the addition of the EXTERNAL NAME clause. For example: CREATE FUNCTION GetEmpFirstLastNames() RETURNS TABLE (FirstName NVARCHAR(4000), LastName NVARCHAR(4000)) EXTERNAL NAME MyDotNETAssembly.[MyNamespace.MyClassname]. GetEmpFirstLastNames; Table-valued functions are used to represent data in relational form for further processing in queries such as: select * from function(); select * from tbl join function() f on tbl.col = f.col; select * from table t cross apply function(t.column); Table-valued functions can return a table when: Created from scalar input arguments. For example, a table-valued function that takes a comma-delimited string of numbers and pivots them into a table. Generated from external data. For example, a table-valued function that reads the event log and exposes it as a table. Note A table-valued function can only perform data access through a Transact-SQL query in the InitMethod method, and not in the FillRow method. The InitMethod should be marked with the SqlFunction.DataAccess.Read attribute property if a Transact-SQL query is performed. A Sample Table-Valued Function The following table-valued function returns information from the system event log. The function takes a single string argument containing the name of the event log to read. Sa mp l e C o d e using using using using using using System; System.Data.Sql; Microsoft.SqlServer.Server; System.Collections; System.Data.SqlTypes; System.Diagnostics; public class TabularEventLog { [SqlFunction(FillRowMethodName = "FillRow")] public static IEnumerable InitMethod(String logname) { return new EventLog(logname).Entries; } public static void FillRow(Object obj, out SqlDateTime timeWritten, out SqlChars message, out SqlChars category, out long instanceId) { EventLogEntry eventLogEntry = (EventLogEntry)obj; timeWritten = new SqlDateTime(eventLogEntry.TimeWritten); message = new SqlChars(eventLogEntry.Message); category = new SqlChars(eventLogEntry.Category); instanceId = eventLogEntry.InstanceId; } } Imports Imports Imports Imports Imports Imports Imports System System.Data.Sql Microsoft.SqlServer.Server System.Collections System.Data.SqlTypes System.Diagnostics System.Runtime.InteropServices Public Class TabularEventLog <SqlFunction(FillRowMethodName:="FillRow")> _ Public Shared Function InitMethod(ByVal logname As String) As IEnumerable Return New EventLog(logname).Entries End Function Public Shared Sub FillRow(ByVal obj As Object, <Out()> ByRef timeWritten As SqlDateTime, <Out()> ByRef message As SqlChars, <Out()> ByRef category As SqlChars, <Out()> ByRef instanceId As Long) Dim eventLogEnTry As EventLogEntry = CType(obj, EventLogEntry) timeWritten = New SqlDateTime(eventLogEnTry.TimeWritten) message = New SqlChars(eventLogEnTry.Message) category = New SqlChars(eventLogEnTry.Category) instanceId = eventLogEnTry.InstanceId End Sub End Class De c l a ri n g a n d Us i n g t h e Sa mp l e Ta b l e -V a l u e d F u n c t i o n After the sample table-valued function has been compiled, it can be declared in Transact-SQL like this: use master; -- Replace SQL_Server_logon with your SQL Server user credentials. GRANT EXTERNAL ACCESS ASSEMBLY TO [SQL_Server_logon]; -- Modify the following line to specify a different database. ALTER DATABASE master SET TRUSTWORTHY ON; -- Modify the next line to use the appropriate database. CREATE ASSEMBLY tvfEventLog FROM 'D:\assemblies\tvfEventLog\tvfeventlog.dll' WITH PERMISSION_SET = EXTERNAL_ACCESS; GO CREATE FUNCTION ReadEventLog(@logname nvarchar(100)) RETURNS TABLE (logTime datetime,Message nvarchar(4000),Category nvarchar(4000),InstanceId bigint) AS EXTERNAL NAME tvfEventLog.TabularEventLog.InitMethod; GO Visual C++ database objects compiled with /clr:pure are not supported for execution on SQL Server 2005. For example, such database objects include table-valued functions. To test the sample, try the following Transact-SQL code: -- Select the top 100 events, SELECT TOP 100 * FROM dbo.ReadEventLog(N'Security') as T; go -- Select the last 10 login events. SELECT TOP 10 T.logTime, T.Message, T.InstanceId FROM dbo.ReadEventLog(N'Security') as T WHERE T.Category = N'Logon/Logoff'; go Sample: Returning the Results of a SQL Server Query The following sample shows a table-valued function that queries a SQL Server database. This sample uses the AdventureWorks Light database from SQL Server 2008. See http://www.codeplex.com/sqlserversamples for more information on downloading AdventureWorks. Name your source code file FindInvalidEmails.cs or FindInvalidEmails.vb. using using using using using System; System.Collections; System.Data; System.Data.SqlClient; System.Data.SqlTypes; using Microsoft.SqlServer.Server; public partial class UserDefinedFunctions { private class EmailResult { public SqlInt32 CustomerId; public SqlString EmailAdress; public EmailResult(SqlInt32 customerId, SqlString emailAdress) { CustomerId = customerId; EmailAdress = emailAdress; } } public static bool ValidateEmail(SqlString emailAddress) { if (emailAddress.IsNull) return false; if (!emailAddress.Value.EndsWith("@adventure-works.com")) return false; // Validate the address. Put any more rules here. return true; } [SqlFunction( DataAccess = DataAccessKind.Read, FillRowMethodName = "FindInvalidEmails_FillRow", TableDefinition="CustomerId int, EmailAddress nvarchar(4000)")] public static IEnumerable FindInvalidEmails(SqlDateTime modifiedSince) { ArrayList resultCollection = new ArrayList(); using (SqlConnection connection = new SqlConnection("context connection=true")) { connection.Open(); using (SqlCommand selectEmails = new SqlCommand( "SELECT " + "[CustomerID], [EmailAddress] " + "FROM [AdventureWorksLT2008].[SalesLT].[Customer] " + "WHERE [ModifiedDate] >= @modifiedSince", connection)) { SqlParameter modifiedSinceParam = selectEmails.Parameters.Add( "@modifiedSince", SqlDbType.DateTime); modifiedSinceParam.Value = modifiedSince; using (SqlDataReader emailsReader = selectEmails.ExecuteReader()) { while (emailsReader.Read()) { SqlString emailAddress = emailsReader.GetSqlString(1); if (ValidateEmail(emailAddress)) { resultCollection.Add(new EmailResult( emailsReader.GetSqlInt32(0), emailAddress)); } } } } } return resultCollection; } public static void FindInvalidEmails_FillRow( object emailResultObj, out SqlInt32 customerId, out SqlString emailAdress) { EmailResult emailResult = (EmailResult)emailResultObj; customerId = emailResult.CustomerId; emailAdress = emailResult.EmailAdress; } }; Imports Imports Imports Imports Imports System.Collections System.Data System.Data.SqlClient System.Data.SqlTypes Microsoft.SqlServer.Server Public Partial Class UserDefinedFunctions Private Class EmailResult Public CustomerId As SqlInt32 Public EmailAdress As SqlString Public EmailAdress As SqlString Public Sub New(customerId__1 As SqlInt32, emailAdress__2 As SqlString) CustomerId = customerId__1 EmailAdress = emailAdress__2 End Sub End Class Public Shared Function ValidateEmail(emailAddress As SqlString) As Boolean If emailAddress.IsNull Then Return False End If If Not emailAddress.Value.EndsWith("@adventure-works.com") Then Return False End If ' Validate the address. Put any more rules here. Return True End Function <SqlFunction(DataAccess := DataAccessKind.Read, FillRowMethodName := "FindInvalidEmails_FillRow", TableDefinition := "CustomerId int, EmailAddress nvarchar(4000)")> _ Public Shared Function FindInvalidEmails(modifiedSince As SqlDateTime) As IEnumerable Dim resultCollection As New ArrayList() Using connection As New SqlConnection("context connection=true") connection.Open() Using selectEmails As New SqlCommand("SELECT " & "[CustomerID], [EmailAddress] " & "FROM [AdventureWorksLT2008].[SalesLT].[Customer] " & "WHERE [ModifiedDate] >= @modifiedSince", connection) Dim modifiedSinceParam As SqlParameter = selectEmails.Parameters.Add("@modifiedSince", SqlDbType.DateTime) modifiedSinceParam.Value = modifiedSince Using emailsReader As SqlDataReader = selectEmails.ExecuteReader() While emailsReader.Read() Dim emailAddress As SqlString = emailsReader.GetSqlString(1) If ValidateEmail(emailAddress) Then resultCollection.Add(New EmailResult(emailsReader.GetSqlInt32(0), emailAddress)) End If End While End Using End Using End Using Return resultCollection End Function Public Shared Sub FindInvalidEmails_FillRow(emailResultObj As Object, ByRef customerId As SqlInt32, ByRef emailAdress As SqlString) Dim emailResult As EmailResult = DirectCast(emailResultObj, EmailResult) customerId = emailResult.CustomerId emailAdress = emailResult.EmailAdress End Sub End ClassImports System.Collections Imports System.Data Imports System.Data.SqlClient Imports System.Data.SqlTypes Imports Microsoft.SqlServer.Server Public Partial Class UserDefinedFunctions Private Class EmailResult Public CustomerId As SqlInt32 Public EmailAdress As SqlString Public Sub New(customerId__1 As SqlInt32, emailAdress__2 As SqlString) CustomerId = customerId__1 EmailAdress = emailAdress__2 End Sub End Class Public Shared Function ValidateEmail(emailAddress As SqlString) As Boolean If emailAddress.IsNull Then Return False End If If Not emailAddress.Value.EndsWith("@adventure-works.com") Then Return False End If ' Validate the address. Put any more rules here. Return True End Function <SqlFunction(DataAccess := DataAccessKind.Read, FillRowMethodName := "FindInvalidEmails_FillRow", TableDefinition := "CustomerId int, EmailAddress nvarchar(4000)")> _ Public Shared Function FindInvalidEmails(modifiedSince As SqlDateTime) As IEnumerable Dim resultCollection As New ArrayList() Using connection As New SqlConnection("context connection=true") connection.Open() Using selectEmails As New SqlCommand("SELECT " & "[CustomerID], [EmailAddress] " & "FROM [AdventureWorksLT2008].[SalesLT].[Customer] " & "WHERE [ModifiedDate] >= @modifiedSince", connection) Dim modifiedSinceParam As SqlParameter = selectEmails.Parameters.Add("@modifiedSince", SqlDbType.DateTime) modifiedSinceParam.Value = modifiedSince Using emailsReader As SqlDataReader = selectEmails.ExecuteReader() While emailsReader.Read() Dim emailAddress As SqlString = emailsReader.GetSqlString(1) If ValidateEmail(emailAddress) Then resultCollection.Add(New EmailResult(emailsReader.GetSqlInt32(0), emailAddress)) End If End While End Using End Using End Using Return resultCollection End Function Public Shared Sub FindInvalidEmails_FillRow(emailResultObj As Object, customerId As SqlInt32, emailAdress As SqlString) Dim emailResult As EmailResult = DirectCast(emailResultObj, EmailResult) customerId = emailResult.CustomerId emailAdress = emailResult.EmailAdress End Sub End Class Compile the source code to a DLL and copy the DLL to the root directory of your C drive. Then, execute the following Transact-SQL query. use AdventureWorksLT2008; go IF EXISTS (SELECT name FROM sysobjects WHERE name = 'FindInvalidEmails') DROP FUNCTION FindInvalidEmails; go IF EXISTS (SELECT name FROM sys.assemblies WHERE name = 'MyClrCode') DROP ASSEMBLY MyClrCode; go CREATE ASSEMBLY MyClrCode FROM 'C:\FindInvalidEmails.dll' WITH PERMISSION_SET = SAFE -- EXTERNAL_ACCESS; GO CREATE FUNCTION FindInvalidEmails(@ModifiedSince datetime) RETURNS TABLE ( CustomerId int, EmailAddress nvarchar(4000) ) AS EXTERNAL NAME MyClrCode.UserDefinedFunctions.[FindInvalidEmails]; go SELECT * FROM FindInvalidEmails('2000-01-01'); go CLR User-Defined Aggregate - Invoking Functions 3/24/2017 • 7 min to read • Edit Online In Transact-SQL SELECT statements, you can invoke common language runtime (CLR) user-defined aggregates, subject to all the rules that apply to system aggregate functions. The following additional rules apply: The current user must have EXECUTE permission on the user-defined aggregate. User-defined aggregates must be invoked using a two-part name in the form of schema_name.udagg_name. The argument type of the user-defined aggregate must match or be implicitly convertible to the input_type of the aggregate, as defined in the CREATE AGGREGATE statement. The return type of the user-defined aggregate must match the return_type in the CREATE AGGREGATE statement. Example 1 The following is an example of a user-defined aggregate function that concatenates a set of string values taken from a column in a table: [C#] using using using using using using System; System.Data; Microsoft.SqlServer.Server; System.Data.SqlTypes; System.IO; System.Text; [Serializable] [SqlUserDefinedAggregate( Format.UserDefined, //use clr serialization to serialize the intermediate result IsInvariantToNulls = true, //optimizer property IsInvariantToDuplicates = false, //optimizer property IsInvariantToOrder = false, //optimizer property MaxByteSize = 8000) //maximum size in bytes of persisted value ] public class Concatenate : IBinarySerialize { /// <summary> /// The variable that holds the intermediate result of the concatenation /// </summary> private StringBuilder intermediateResult; /// <summary> /// Initialize the internal data structures /// </summary> public void Init() { this.intermediateResult = new StringBuilder(); } /// /// /// /// <summary> Accumulate the next value, not if the value is null </summary> <param name="value"></param> /// <param name="value"></param> public void Accumulate(SqlString value) { if (value.IsNull) { return; } this.intermediateResult.Append(value.Value).Append(','); } /// <summary> /// Merge the partially computed aggregate with this aggregate. /// </summary> /// <param name="other"></param> public void Merge(Concatenate other) { this.intermediateResult.Append(other.intermediateResult); } /// <summary> /// Called at the end of aggregation, to return the results of the aggregation. /// </summary> /// <returns></returns> public SqlString Terminate() { string output = string.Empty; //delete the trailing comma, if any if (this.intermediateResult != null && this.intermediateResult.Length > 0) { output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1); } return new SqlString(output); } public void Read(BinaryReader r) { intermediateResult = new StringBuilder(r.ReadString()); } public void Write(BinaryWriter w) { w.Write(this.intermediateResult.ToString()); } } [Visual Basic] Imports Imports Imports Imports Imports Imports System System.Data Microsoft.SqlServer.Server System.Data.SqlTypes System.IO System.Text <Serializable(), SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToNulls:=True, IsInvariantToDuplicates:=False, IsInvariantToOrder:=False, MaxByteSize:=8000)> _ Public Class Concatenate Implements IBinarySerialize ''' <summary> ''' The variable that holds the intermediate result of the concatenation ''' </summary> Private intermediateResult As StringBuilder ''' <summary> ''' Initialize the internal data structures ''' </summary> Public Sub Init() Me.intermediateResult = New StringBuilder() End Sub ''' <summary> ''' Accumulate the next value, not if the value is null ''' </summary> ''' <param name="value"></param> Public Sub Accumulate(ByVal value As SqlString) If value.IsNull Then Return End If Me.intermediateResult.Append(value.Value).Append(","c) End Sub ''' <summary> ''' Merge the partially computed aggregate with this aggregate. ''' </summary> ''' <param name="other"></param> Public Sub Merge(ByVal other As Concatenate) Me.intermediateResult.Append(other.intermediateResult) End Sub ''' <summary> ''' Called at the end of aggregation, to return the results of the aggregation. ''' </summary> ''' <returns></returns> Public Function Terminate() As SqlString Dim output As String = String.Empty 'delete the trailing comma, if any If Not (Me.intermediateResult Is Nothing) AndAlso Me.intermediateResult.Length > 0 Then output = Me.intermediateResult.ToString(0, Me.intermediateResult.Length - 1) End If Return New SqlString(output) End Function Public Sub Read(ByVal r As BinaryReader) Implements IBinarySerialize.Read intermediateResult = New StringBuilder(r.ReadString()) End Sub Public Sub Write(ByVal w As BinaryWriter) Implements IBinarySerialize.Write w.Write(Me.intermediateResult.ToString()) End Sub End Class Once you compile the code into MyAgg.dll, you can register the aggregate in SQL Server as follows: CREATE ASSEMBLY MyAgg FROM 'C:\MyAgg.dll'; GO CREATE AGGREGATE MyAgg (@input nvarchar(200)) RETURNS nvarchar(max) EXTERNAL NAME MyAgg.Concatenate; NOTE Visual C++ database objects, such as scalar-valued functions, that have been compiled with the /clr:pure compiler option are not supported for execution in SQL Server. As with most aggregates, the bulk of the logic is in the Accumulate method. Here, the string that is passed in as a parameter to the Accumulate method is appended to the StringBuilder object that was initialized in the Init method. Assuming that this is not the first time the Accumulate method has been called, a comma is also appended to the StringBuilder prior to appending the passed-in string. At the conclusion of the computational tasks, the Terminate method is called, which returns the StringBuilder as a string. For example, consider a table with the following schema: CREATE TABLE BookAuthors ( BookID int NOT NULL, AuthorName nvarchar(200) NOT NULL ); Then insert the following rows: INSERT BookAuthors VALUES(1, 'Johnson'),(2, 'Taylor'),(3, 'Steven'),(2, 'Mayler'),(3, 'Roberts'),(3, 'Michaels'); The following query would then produce the following result: SELECT BookID, dbo.MyAgg(AuthorName) FROM BookAuthors GROUP BY BookID; BOOKID AUTHOR NAMES 1 Johnson 2 Taylor, Mayler 3 Roberts, Michaels, Steven Example 2 The following sample shows an aggregate that has two parameters on the Accumulate method. [C#] using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; [Serializable] [SqlUserDefinedAggregate( Format.Native, IsInvariantToDuplicates = false, IsInvariantToNulls = true, IsInvariantToOrder = true, IsNullIfEmpty = true, Name = "WeightedAvg")] public struct WeightedAvg { /// <summary> /// The variable that holds the intermediate sum of all values multiplied by their weight /// </summary> private long sum; /// <summary> /// The variable that holds the intermediate sum of all weights /// </summary> private int count; /// <summary> /// Initialize the internal data structures /// </summary> public void Init() { sum = 0; count = 0; } /// <summary> /// Accumulate the next value, not if the value is null /// </summary> /// <param name="Value">Next value to be aggregated</param> /// <param name="Weight">The weight of the value passed to Value parameter</param> public void Accumulate(SqlInt32 Value, SqlInt32 Weight) { if (!Value.IsNull && !Weight.IsNull) { sum += (long)Value * (long)Weight; count += (int)Weight; } } /// <summary> /// Merge the partially computed aggregate with this aggregate /// </summary> /// <param name="Group">The other partial results to be merged</param> public void Merge(WeightedAvg Group) { sum += Group.sum; count += Group.count; } /// <summary> /// Called at the end of aggregation, to return the results of the aggregation. /// </summary> /// <returns>The weighted average of all inputed values</returns> public SqlInt32 Terminate() { if (count > 0) { int value = (int)(sum / count); return new SqlInt32(value); } else { { return SqlInt32.Null; } } } [Visual Basic] Imports Imports Imports Imports Imports Imports System System.Data System.Data.SqlClient System.Data.SqlTypes Microsoft.SqlServer.Server System.Runtime.InteropServices <StructLayout(LayoutKind.Sequential)> _ <Serializable(), SqlUserDefinedAggregate(Format.Native, _ IsInvariantToDuplicates:=False, _ IsInvariantToNulls:=True, _ IsInvariantToOrder:=True, _ IsNullIfEmpty:=True, _ Name:="WeightedAvg")> _ Public Class WeightedAvg ''' <summary> ''' The variable that holds the intermediate sum of all values multiplied by their weight ''' </summary> Private sum As Long ''' <summary> ''' The variable that holds the intermediate sum of all weights ''' </summary> Private count As Integer ''' <summary> ''' The variable that holds the intermediate sum of all weights ''' </summary> Public Sub Init() sum = 0 count = 0 End Sub ''' <summary> ''' Accumulate the next value, not if the value is null ''' </summary> ''' <param name="Value">Next value to be aggregated</param> ''' <param name="Weight">The weight of the value passed to Value parameter</param> Public Sub Accumulate(ByVal Value As SqlInt32, ByVal Weight As SqlInt32) If Not Value.IsNull AndAlso Not Weight.IsNull Then sum += CType(Value, Long) * CType(Weight, Long) count += CType(Weight, Integer) End If End Sub ''' <summary> ''' Merge the partially computed aggregate with this aggregate. ''' </summary> ''' <param name="Group">The other partial results to be merged</param> Public Sub Merge(ByVal Group As WeightedAvg) sum = Group.sum count = Group.count End Sub ''' <summary> ''' Called at the end of aggregation, to return the results of the aggregation. ''' </summary> ''' <returns>The weighted average of all inputed values</returns> Public Function Terminate() As SqlInt32 Public Function Terminate() As SqlInt32 If count > 0 Then '' int value = (int)(sum / count); '' return new SqlInt32(value); Dim value As Integer = CType(sum / count, Integer) Return New SqlInt32(value) Else Return SqlInt32.Null End If End Function End Class After you compile the C# or Visual Basic source code, run the following Transact-SQL. This script assumes that the DLL is called WghtAvg.dll and is in the root directory of your C drive. A database called test is also assumed. use test; go -- sp_configure 'clr enabled', 1; -- go --- RECONFIGURE WITH OVERRIDE; -- go IF EXISTS (SELECT name FROM systypes WHERE name = 'MyTableType') DROP TYPE MyTableType; go IF EXISTS (SELECT name FROM sysobjects WHERE name = 'WeightedAvg') DROP AGGREGATE WeightedAvg; go IF EXISTS (SELECT name FROM sys.assemblies WHERE name = 'MyClrCode') DROP ASSEMBLY MyClrCode; go CREATE ASSEMBLY MyClrCode FROM 'C:\WghtAvg.dll'; GO CREATE AGGREGATE WeightedAvg (@value int, @weight int) RETURNS int EXTERNAL NAME MyClrCode.WeightedAvg; go CREATE TYPE MyTableType AS table (ItemValue int, ItemWeight int); go DECLARE @myTable AS MyTableType; INSERT INTO @myTable VALUES(1, 4), (6, 1); SELECT dbo.WeightedAvg(ItemValue, ItemWeight) FROM @myTable; go See Also CLR User-Defined Aggregates CLR User-Defined Aggregates - Requirements 3/24/2017 • 3 min to read • Edit Online A type in a common language runtime (CLR) assembly can be registered as a user-defined aggregate function, as long as it implements the required aggregation contract. This contract consists of the SqlUserDefinedAggregate attribute and the aggregation contract methods. The aggregation contract includes the mechanism to save the intermediate state of the aggregation, and the mechanism to accumulate new values, which consists of four methods: Init, Accumulate, Merge, and Terminate. When you have met these requirements, you will be able to take full advantage of user-defined aggregates in Microsoft SQL Server. The following sections of this topic provide additional details about how to create and work with user-defined aggregates. For an example, see Invoking CLR User-Defined Aggregate Functions. SqlUserDefinedAggregate For more information, see SqlUserDefinedAggregateAttribute. Aggregation Methods The class registered as a user-defined aggregate should support the following instance methods. These are the methods that the query processor uses to compute the aggregation: METHOD Init SYNTAX public void Init(); DESCRIPTION The query processor uses this method to initialize the computation of the aggregation. This method is invoked once for each group that the query processor is aggregating. The query processor may choose to reuse the same instance of the aggregate class for computing aggregates of multiple groups. The Init method should perform any clean-up as necessary from previous uses of this instance, and enable it to re-start a new aggregate computation. METHOD Accumulate SYNTAX public void Accumulate ( inputtype value[, input-type value, ...]); DESCRIPTION One or more parameters representing the parameters of the function. input_type should be the managed SQL Server data type equivalent to the native SQL Server data type specified by input_sqltype in the CREATE AGGREGATE statement. For more information, see Mapping CLR Parameter Data. For user-defined types (UDTs), the input-type is the same as the UDT type. The query processor uses this method to accumulate the aggregate values. This is invoked once for each value in the group that is being aggregated. The query processor always calls this only after calling the Init method on the given instance of the aggregate-class. The implementation of this method should update the state of the instance to reflect the accumulation of the argument value being passed in. Merge public void Merge( udagg_class value); This method can be used to merge another instance of this aggregate class with the current instance. The query processor uses this method to merge multiple partial computations of an aggregation. Terminate public return_type Terminate(); This method completes the aggregate computation and returns the result of the aggregation. The return_type should be a managed SQL Server data type that is the managed equivalent of return_sqltype specified in the CREATE AGGREGATE statement. The return_type can also be a user-defined type. Table -Valued Parameters Table-valued parameters (TVPs), user-defined table types that are passed into a procedure or function, provide an efficient way to pass multiple rows of data to the server. TVPs provide similar functionality to parameter arrays, but offer greater flexibility and closer integration with Transact-SQL. They also provide the potential for better performance. TVPs also help reduce the number of round trips to the server. Instead of sending multiple requests to the server, such as with a list of scalar parameters, data can be sent to the server as a TVP. A user-defined table type cannot be passed as a table-valued parameter to, or be returned from, a managed stored procedure or function executing in the SQL Server process. Also, TVPs cannot be used within the scope of a context connection. However, a TVP can be used with SqlClient in managed stored procedures or functions executing in the SQL Server process, if it is used in a connection that is not a context connection. The connection can be to the same server that is executing the managed procedure or function. For more information about TVPs, see Use Table-Valued Parameters (Database Engine). Change History UPDATED CONTENT Updated the description of the Accumulate method; it now accepts more than one parameter. See Also CLR User-Defined Types Invoking CLR User-Defined Aggregate Functions CLR User-Defined Aggregates 3/24/2017 • 1 min to read • Edit Online Aggregate functions perform a calculation on a set of values and return a single value. Traditionally, Microsoft SQL Server has supported only built-in aggregate functions, such as SUM or MAX, that operate on a set of input scalar values and generate a single aggregate value from that set. SQL Server integration with the Microsoft .NET Framework common language runtime (CLR) now allows developers to create custom aggregate functions in managed code, and to make these functions accessible to Transact-SQL or other managed code. The following table lists the topics in this section. Requirements for CLR User-Defined Aggregates Provides an overview of the requirements for implementing CLR user-defined aggregate functions. Invoking CLR User-Defined Aggregate Functions Explains how to invoke user-defined aggregates. CLR User-Defined Functions 3/24/2017 • 1 min to read • Edit Online User-defined functions are routines that can take parameters, perform calculations or other actions, and return a result. Beginning with SQL Server 2005, you can write user-defined functions in any Microsoft .NET Framework programming language, such as Microsoft Visual Basic .NET or Microsoft Visual C#. There are two types of functions: scalar, which returns a single value, and table-valued, which returns a set of rows. The following table lists the topics in this section. CLR Scalar-Valued Functions Covers implementation requirements and examples of scalar-valued functions. CLR Table-Valued Functions Discusses how to implement and use table-valued functions (TVFs), as well as differences between Transact-SQL and common language runtime (CLR) TVFs. CLR User-Defined Aggregates Describes how to implement and use user-defined aggregates. See Also User-Defined Functions