Download CLR Scalar-Valued Functions | Microsoft Docs

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts

Expectation–maximization algorithm wikipedia , lookup

Transcript
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