Обсудить
бизнес-задачи

Использование технологии SQL CLR для увеличения производительности функционала MS SQL

блог о bi, №1 в рунете
Крайне важно при работе с крупной аналитической системой обеспечить ее быстродействие на всех этапах работы. Однако не всегда встроенный функционал MS SQL может выполнять операции с данными оперативно.
Например, при работе с большими данными крайне не рекомендуется манипулировать ими при помощи пользовательских функций, поскольку это очень сильно замедляет работу программы. Это происходит потому что MS SQL использует теоретико-множественную парадигму программирования, то есть данный язык манипулирует с данными как с объектами множества. Он хорош в операциях извлечения данных и работой с ними как набором записей. Пользовательские функции для MS SQL представляют собой «черный ящик» и при работе с данными выполняет операции построчно.

Один из способов улучшить скорость работы таких функций – использовать технологию SQL CLR.

Она позволяет расширять функциональность SQL сервера с помощью высокоуровневых языков программирования, например C# или VB.NET, путем создания пользовательских хранимых процедур, триггеров, типов данных и функций на высокопроизводительных языках программирования. Это позволяет значительно повысить производительность и интегрировать мощные алгоритмы и операции, недоступные в рамках чистого SQL, тем самым многократно расширяя возможности SQL Server.

В данной статье, мы сравним две реализации алгоритмов разбиения строки на подстроки инструментами C# и T-SQL.

Рассмотрим реализацию алгоритма в MS SQL (Листинг 1). На вход функция принимает строку, которую будем разбивать на подстроки, и строку-разделитель. На выход функция возвращает таблицу с номером подстроки в таблице и саму подстроку.
Листинг 1. Реализация алгоритма на T-SQL
ALTER Function [dbo].[split](
   @InputText Varchar(max),
   @Delimiter Varchar(10)) 


RETURNS @Array TABLE (
   TokenID Int PRIMARY KEY IDENTITY(1,1),
   Value Varchar(max))


AS
BEGIN


   DECLARE
      @Pos Int,        
      @End Int,       
      @TextLength Int, 
      @DelimLength Int 


   SET @TextLength = DataLength(@InputText)
   IF @TextLength = 0 RETURN
   SET @Pos = 0
   SET @DelimLength = DataLength(@Delimiter)


   IF @DelimLength = 0 BEGIN 
      WHILE @Pos <= @TextLength BEGIN
         INSERT @Array (Value) VALUES (SubString(@InputText,@Pos,1))
         SET @Pos = @Pos + 1
      END
   END
   ELSE BEGIN
      -- Tack on delimiter to 'see' the last token
      SET @InputText = @InputText + @Delimiter
      -- Find the end character of the first token
      SET @End = CharIndex(@Delimiter, @InputText)
      WHILE @End > 0 BEGIN
         -- End > 0, a delimiter was found: there is a(nother) token
         INSERT @Array (Value) VALUES (SubString(@InputText, @Pos, @End - @Pos))
         -- Set next search to start after the previous token
         SET @Pos = @End + @DelimLength
         -- Find the end character of the next token
         SET @End = CharIndex(@Delimiter, @InputText, @Pos)
      END
   END
   RETURN
END
Теперь создадим аналогичную функцию в среде Visual Studio на языке C#.
Для этого в Visual Studio создаем проект базы данных SQL Server.
Затем через обозреватель решений добавляем определяемую пользователем CLR функцию (Скрин 1, 2).
Скрин 1. Проект => Добавить => Скрипт
Скрин 2. SQL CLR C# => Пользовательская функция
Теперь можно реализовывать сам алгоритм (Листинг 2).
Листинг 2. Реализация алгоритма на C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;


public partial class UserDefinedFunctions
{
    [SqlFunction(FillRowMethodName = "SplitStringFillRow", TableDefinition = "part NVARCHAR(MAX), ID_ORDER INT")]
    static public IEnumerator SplitString(string text, string delimiter)


    {
        int text_len = text.Length;
        int delimeter_len = delimiter.Length;
        int pos = 0;
        int count = 0;


        if (text_len == 0)
        {
            yield break;
        }


        if (delimeter_len == 0)
        {
            for(int i = 0; i < text_len; i++)
            {
                yield return new KeyValuePair<int, string>(i+1, text[i].ToString());
            }
        }
        else
        {
            text = text + delimiter;
            int end = text.IndexOf(delimiter);


            if (text.Substring(pos, end - pos + delimeter_len) == delimiter)
            {
                count += 1;
                yield return new KeyValuePair<int, string>(count, text.Substring(pos, end - pos));
                pos = end + delimeter_len;
                end = text.IndexOf(delimiter, pos);
            }


            while (end > 0)
            {
                count += 1;
                yield return new KeyValuePair<int, string>(count, text.Substring(pos, end - pos));
                pos = end + delimeter_len;
                end = text.IndexOf(delimiter, pos);


            }
        }


    }
    static public void SplitStringFillRow(object oKeyValuePair, out SqlString value, out SqlInt32 valueIndex)
    {
        KeyValuePair<int, string> keyValuePair = (KeyValuePair<int, string>)oKeyValuePair;
        valueIndex = keyValuePair.Key;
        value = keyValuePair.Value;
    }
}
Прежде чем приступить к сравнению двух алгоритмов нужно настроить интеграцию CLR функции в среду MS SQL. Для этого:

  1. В Visual Studio собираем проект
  2. В MS SQL вашей базе данных настраиваем подключение внешней сборки (Скрин 3, 4)
Скрин 3. Assemblies => New Assembly
Скрин 4. В поле Path введите путь до файла dll в вашем проекте с CLR функцией
Замечание:
Если вы работаете не на локальном сервере, то нужно будет убедиться, что владелец базы данных, записанный в master, совпадает с владельцем, записанной в вашей базе данных. В противном случае, воспользуйтесь следующей командой (Листинг 3)
Листинг 3. Скрипт для смены владельца в базе данных
DECLARE @Command VARCHAR(MAX) = 'ALTER AUTHORIZATION ON DATABASE::<databasename> TO 
[<<loginname>>]' 


SELECT @Command = REPLACE(REPLACE(@Command 
            , '<databasename>', SD.Name)
            , '<loginname>', SL.Name)
FROM master..sysdatabases SD 
JOIN master..syslogins SL ON  SD.SID = SL.SID
WHERE  SD.Name = DB_NAME()


EXEC(@Command) </loginname>
Теперь можно создавать саму функцию (Листинг 4)
Листинг 4. Скрипт для создания функции в MS SQL
CREATE FUNCTION [dbo].SplitStringCLR(@text [nvarchar](max), @delimiter [nvarchar](max))
RETURNS TABLE (
part nvarchar(max),
ID_ODER int
) WITH EXECUTE AS CALLER
AS 
EXTERNAL NAME Database2.UserDefinedFunctions.SplitString*/
Протестируем работу функций на разном объеме данных. Для удобства, все результаты запишем в таблицу.

Количество строк

на вход

Время работы

CLR функции

Время работы

T-SQL функции

10000

00:00:02
00:00:14

100000

00:00:16
00:02:06

1000000

00:02:40
00:21:32
Таким образом, мы видим, что использование технологии SQL CLR может улучшить производительность запросов и кратно ускорить время их выполнение.
Это хорошее преимущество при работе с бизнес-логикой, когда требуется быстро обработать важную информацию большого объема.