Introduction
A few days ago, I was working with String Join and Split Functionality in SQL Server. From that day on, I was thinking of creating custom aggregate functions. Today, we are going to do it by using the CLR feature of SQL Server.
Work Flow
What are we going to do?
- Enable CLR in SQL Server
- Create a CLR Assembly
- Load/Reload/Remove CLR assembly to SQL Server
- Create/Alter/Drop a SQL Server Aggregate/Function/Sp and reload CLR assembly
CLR Configuration
Let us check if CLR is enabled in our SQL Server or not. If not, we are going to enable it.
Enable/Disable
sp_configure 'show advanced options', 1;
GO
RECONFIGURE;
GO
sp_configure 'clr enabled', 1;
GO
RECONFIGURE;
GO
To disable the feature, we have to use 0 rather than 1.
Status Check
SELECT * FROM sys.configurations;
SELECT * FROM sys.configurations WHERE name = 'clr enabled';
EXEC sp_configure;
EXEC sp_configure 'clr enabled';
Build CLR Assembly
We have to create/add an empty library project under any solution and build that project in release mode. We are going to use the generated DLL from the bin folder, which will be loaded as CLR assembly to any SQL Server. If any change is made to CLR codes, that DLL need to be reloaded to SQL Server.
Load CLR Assembly or DLL
Loading/reloading/removing is as simple as given below.
Create
- Give a name (
ClrCommon
) to the assembly or DLL. - Load the DLL from a location.
GO
CREATE ASSEMBLY ClrCommon
FROM 'D:\Study\Sql Server\CLR\ClrExtensions\Clr.Common\bin\Release\Clr.Common.dll'
WITH PERMISSION_SET=SAFE;
GO
Update
- Alter existing assembly (
ClrCommon
). - Reload the DLL from a location.
GO
ALTER ASSEMBLY ClrCommon
FROM 'D:\Study\Sql Server\CLR\ClrExtensions\Clr.Common\bin\Release\Clr.Common.dll'
WITH PERMISSION_SET=SAFE;
GO
Remove
GO
DROP ASSEMBLY ClrCommon;
GO
Aggregate Function
Tmpl
Template class which can be used to create an aggregate function.
using System;
using System.Data;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.IO;
using System.Text;
using System.Collections.Generic;
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined,
IsInvariantToDuplicates = false,
IsInvariantToNulls = true,
IsInvariantToOrder = true,
IsNullIfEmpty = true,
MaxByteSize = 8000,
Name = "The name of the aggregate")]
public class AggregateFunctionTmpl : IBinarySerialize
{
public void Init()
{
}
public void Accumulate(SqlString value)
{
}
public void Merge(AggregateFunctionTmpl group)
{
}
public SqlString Terminate()
{
return new SqlString("Result");
}
public void Read(BinaryReader r)
{
if (r == null)
{
throw new ArgumentNullException("r");
}
}
public void Write(BinaryWriter w)
{
if (w == null)
{
throw new ArgumentNullException("w");
}
}
}
Example: String Join
Using that template, we are creating a class to manage the string join
operation. This class will be called by an aggregate function which will combine column values using a specified separator in SQL Server.
using System;
using System.Data;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.IO;
using System.Text;
using System.Collections.Generic;
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined,
IsInvariantToDuplicates = false,
IsInvariantToNulls = true,
IsInvariantToOrder = true,
IsNullIfEmpty = true,
MaxByteSize = 8000,
Name = "STRING_JOIN_AGG")]
public class StringJoinAgg : IBinarySerialize
{
public const string DefaultSeparator = ",";
class StringJoinModel
{
public string Value { get; set; }
public string Separator { get; set; }
}
private Queue<StringJoinModel> _values;
public void Init()
{
_values = new Queue<StringJoinModel>();
}
public void Accumulate(SqlString value, SqlString separator)
{
if (value.IsNull || String.IsNullOrEmpty(value.Value))
{
return;
}
string valueString = value.Value;
string separatorString = separator.IsNull ||
String.IsNullOrEmpty(separator.Value) ? DefaultSeparator : separator.Value;
_values.Enqueue(new StringJoinModel
{ Value = valueString, Separator = separatorString });
}
public void Merge(StringJoinAgg group)
{
while (group._values.Count != 0)
{
_values.Enqueue(group._values.Dequeue());
}
}
public SqlString Terminate()
{
StringBuilder builder = new StringBuilder();
StringJoinModel model;
if (_values.Count != 0)
{
model = _values.Dequeue();
builder.Append(model.Value);
}
while (_values.Count != 0)
{
model = _values.Dequeue();
builder.Append(model.Separator).Append(model.Value);
}
string value = builder.ToString(); return new SqlString(value);
}
public void Read(BinaryReader r)
{
if (r == null)
{
throw new ArgumentNullException("r");
}
_values = new Queue<StringJoinModel>();
var count = r.ReadInt32();
for (int i = 0; i < count; i++)
{
var model = new StringJoinModel
{
Value = r.ReadString(),
Separator = r.ReadString()
};
_values.Enqueue(model);
}
}
public void Write(BinaryWriter w)
{
if (w == null)
{
throw new ArgumentNullException("w");
}
w.Write(_values.Count);
while(_values.Count != 0)
{
StringJoinModel m = _values.Dequeue();
w.Write(m.Value);
w.Write(m.Separator);
}
}
}
Load/Reload LLD
Please check Build CLR Assembly and Load CLR Assembly Or DLL section to build/rebuild and load/reload DLL from bin to SQL Server.
Create Function in SQL Server
Creating an aggregate function in SQL Server using the CLR class:
GO
CREATE AGGREGATE STRING_JOIN_AGG(@value NVARCHAR(MAX), @separator NVARCHAR(MAX))
RETURNS NVARCHAR(MAX)
EXTERNAL NAME [ClrCommon].[StringJoinAgg];
GO
GO
DROP AGGREGATE STRING_JOIN_AGG;
GO
Usage
Checking the created aggregate function in action:
DECLARE @tblUserGroup TABLE (GroupId INT, UserId INT NULL);
INSERT INTO @tblUserGroup VALUES
(1, 1), (1, 2), (1, 3),
(2, 4), (2, 5), (2, 6),
(3, Null),
(4, 1), (4, 1), (4, 2), (4, 2);
SELECT * FROM @tblUserGroup;
SELECT
GroupId,
COUNT(DISTINCT UserId) AS TotalUser,
dbo.STRING_JOIN_AGG(DISTINCT CONVERT(NVARCHAR(MAX), UserId), ', ') AS UserIds
FROM @tblUserGroup AS g
GROUP BY GroupId;
Working as expected, right!
Limitations
OVER(PARTITION BY GroupId)
works, but OVER(ORDER BY UserId DESC)
is not supported.
Why?
But we can do:
SELECT
GroupId,
COUNT(UserId) AS TotalUser,
dbo.STRING_JOIN_AGG(CONVERT(NVARCHAR(MAX), UserId), ', ') AS UserIds
FROM (
SELECT TOP 99.99999 PERCENT *
FROM @tblUserGroup
ORDER BY GroupId DESC, UserId DESC
) AS g
GROUP BY GroupId;
Table-Valued Function
Tmpl
Template class which can be used to create a table-valued function.
using System;
using System.IO;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections.Generic;
public class TableValuedFunctionTmpl
{
[SqlFunction(
FillRowMethodName = "FillRow",
TableDefinition = "Value NVARCHAR(MAX) NULL, Position BIGINT")]
public static IEnumerable ExecutorMethod(SqlString value)
{
ArrayList list = new ArrayList();
return list;
}
public static void FillRow(Object obj, out SqlString value, out SqlInt64 position)
{
var rowItem = obj;
value = new SqlString("");
position = new SqlInt64(1);
}
}
Example: String Split
Creating a class using that template to manage string split operation. This class will be called by a table-valued function which will split a string
using a specified separator in SQL Server.
using System;
using System.IO;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections.Generic;
public class StringSplitFun
{
public const string DefaultSeparator = ",";
private class StringSplitModel
{
public string Value { get; }
public long Position { get; }
public StringSplitModel(string value, long position)
{
Value = value;
Position = position;
}
}
[SqlFunction(
FillRowMethodName = "SplitFillRow",
TableDefinition = "Value NVARCHAR(MAX) NULL, Position BIGINT")]
public static IEnumerable Split(SqlString values, SqlString separator)
{
List<StringSplitModel> list = new List<StringSplitModel>();
if (values.IsNull || String.IsNullOrEmpty(values.Value))
{
return list;
}
string separatorString = separator.IsNull ||
String.IsNullOrEmpty(separator.Value) ? DefaultSeparator : separator.Value;
String[] valueList = values.Value.Split
(new String[] { separatorString }, StringSplitOptions.None);
for (long i = 0; i < valueList.Length; i++)
{
string value = valueList[i].Trim().ToUpper().Equals("NULL") ?
null : valueList[i];
list.Add(new StringSplitModel(value, i));
}
return list;
}
public static void SplitFillRow(Object obj, out SqlString value, out SqlInt64 position)
{
StringSplitModel entity = (StringSplitModel)obj;
value = new SqlString(entity.Value);
position = new SqlInt64(entity.Position);
}
}
Load/Reload LLD
Please check Build CLR Assembly and Load CLR Assembly Or DLL section to build/rebuild and load/reload DLL from bin to SQL Server.
Create Function in SQL Server
GO
CREATE FUNCTION STRING_SPLIT_FUN(@values NVARCHAR(MAX), @separator NVARCHAR(MAX))
RETURNS TABLE
(
[Value] NVARCHAR(MAX) NULL,
Position BIGINT
)
AS
EXTERNAL NAME ClrCommon.StringSplitFun.Split;
GO
GO
DROP FUNCTION STRING_SPLIT_FUN
GO
Usage
DECLARE @valueList NVARCHAR(MAX) = 'Dan, Han, Mos, Ben ,, ,null, Null, NULL,Tom';
DECLARE @separator NVARCHAR(MAX) = ',';
SELECT
*,
LTRIM(RTRIM(Value)) AS TrimedValue
FROM STRING_SPLIT_FUN(@valueList, @separator)
WHERE Value IS NOT NULL
AND LTRIM(RTRIM(Value)) <> ''
Scalar-Valued Function
Tmpl
Template class which can be used to create a scalar-valued function.
using Microsoft.SqlServer.Server;
using System.Data.SqlClient;
using System.Data.SqlTypes;
public class ScalarValuedFunctionTmpl
{
[SqlFunction]
public static SqlString ExecutorMethod(SqlInt64 value)
{
return new SqlString("Result");
}
}
Example: Number Odd or Even
Using that template, creating a class which will be called by a scalar-valued function. It will return information based on logic and inputs in SQL Server. We can also add more input parameters as needed.
using Microsoft.SqlServer.Server;
using System.Data.SqlClient;
using System.Data.SqlTypes;
public class NumberFun
{
[SqlFunction]
public static SqlString OddOrEven(SqlInt64 number)
{
if (number.IsNull)
{
return new SqlString(null);
}
string value = number.Value%2 == 0 ? "Even" : "Odd";
return new SqlString(value);
}
}
Load/Reload LLD
Please check Build CLR Assembly and Load CLR Assembly Or DLL section to build/rebuild and load/reload DLL from bin to SQL Server.
Create Function In SQL Server
GO
CREATE FUNCTION OddOrEven(@number BIGINT NULL) RETURNS NVARCHAR(MAX)
AS EXTERNAL NAME ClrCommon.NumberFun.OddOrEven;
GO
GO
DROP FUNCTION OddOrEven
GO
Usage
DECLARE @tblNumber TABLE(Value BIGINT NULL);
INSERT INTO @tblNumber(Value) VALUES (1), (NULL), (2);
SELECT
Value,
dbo.OddOrEven(Value) AS Details
FROM @tblNumber;
References
CLR
Load ASSEMBLY or DLL
Aggregate
Function
Table-Valued Function
Scalar-Valued Function
YouTube
Limitations
This is a get started or learning purpose post. The code may throw unexpected errors for untested inputs. If any, just let me know.
Please find Visual Studio 2017 solution as an attachment, including all template and SQL files. If there is any build error, let me know.
History
- 2nd July, 2019: Initial version