SQL Server: Erweiterung des Funktionsumfangs mittels F# Libraries

Der SQL Server liefert eine Vielzahl von Funktionen. Trotzdem steht man zuweilen vor Problemen, die sich nicht oder nur umständlich mit SQL Server Funktionen lösen lassen. Ein Beispiel hierfür ist die Durchführung einer „unscharfen“ Stringsuche. Mit „unscharfer“ Suche bezeichne ich hier die Suche nach ähnlichen Zeichenfolgen. So soll z.B. bei einer Suche nach dem String „Müller“ auch der String „Müler“ getroffen werden.


Zu diesem Zweck bietet der SQL Server die Möglichkeit eigene Funktionen in Form von .NET Programmbibliotheken einzubinden. In diesem Blogbeitrag werde ich diese Möglichkeit am Beispiel der Levenshtein-Distanz demonstrieren. Die Levenshtein-Distanz ist eine bekannte Methode zur Berechnung der Ähnlichkeit zweier Zeichenketten. Sie basiert auf dem Prinzip, zu bestimmen wie viele Zeichenänderungen notwendig sind, um eine Zeichenkette in eine andere umzuwandeln. Je weniger Zeichenänderungen notwendig sind, desto ähnlicher sind sich zwei Zeichenketten.

 

Ein Beispiel (passend zur Weihnachtszeit):

 

s1: Weihnachtsbaum

 

s2: Weihnachtbaum

 

s3: Weihnachtsmarkt

 

 

Um beispielsweise s1 in die Zeichenkette s2 umzuwandeln ist genau eine Operation notwendig (entferne Zeichen ‚s‘). Für die Umwandlung von s1 nach s3 sind vier Umwandlungen notwendig (Tauschen von ‚b‘, ‚u‘ und ‚m‘ sowie das Hinzufügen von ‚t‘ ans Ende der Zeichenkette). Somit sind sich s1 und s2 ähnlicher als s1 und s3.

 

 

In diesem Blogbeitrag will ich anhand der Programmiersprache F# demonstrieren, wie sich dieser Algorithmus in SQL Server einbinden lässt. F# ist eine funktionale Programmiersprache, die sich besonders für die Umsetzung von mathematischen Problemen eignet.

 

 

Eine einfache Implementierung der Levenshtein-Distanz sieht in F# folgendermaßen aus:

(*Funktion min gibt von drei Werten den geringsten Wert zurück*)
let inline min val1 val2 val3 =
    if val1 < val2 && val1 < val3 then val1
    elif val2 < val3 then val2
    else val3

(*Funktion levenshtein gibt die Levenshtein-Distanz von zwei Worten zurück*)
let levenshtein (u:string) (v:string) =
    let m = u.Length //Länge wort1
    let n = v.Length //Länge wort2
    let d = Array2D.create (m+1) (n+1) -1 //Matrix für die Verarbeitung erstellen und mit -1 initialisieren
    //Die rekursive Funktion dist geht durch das zweidimensionale Array und sucht den Weg mit den wenigsten Tauschoperationen
    let rec dist =
        function
        | i, 0 -> i
        | 0, j -> j
        | i,j when d.[i,j] <> -1 -> d.[i,j]
        | i,j ->
            let dval =
                if u.[i-1] = v.[j-1] then dist (i-1, j-1)
                else
                    min 
                        (dist(i-1,j)+1) //deletion
                        (dist(i,j-1)+1) //insertion
                        (dist(i-1,j-1)+1) //substitution
            d.[i,j] <- dval; dval //Aktualisiert den Zellenwert im Array
    dist (m,n) //Der letzte Wert der Matrix wird zurückgegeben

Zur Veranschaulichung hier nochmal die Arbeitsweise des Algorithmus in grafischer Form:

 

Zuerst wird ein Array erzeugt, dass eine Matrix aufspannt und mit -1 initialisiert wird.
Zuerst wird ein Array erzeugt, dass eine Matrix aufspannt und mit -1 initialisiert wird.

 

 

Die rekursive Funktion geht nun durch das Array beginnend links oben und sucht den günstigsten Weg (Den mit den wenigsten Änderungsoperationen).
Die rekursive Funktion geht nun durch das Array beginnend links oben und sucht den günstigsten Weg (Den mit den wenigsten Änderungsoperationen).

 

 

Das fertige Array sieht dann letztendlich so aus. In der rechten unteren Ecke erhalten wir das Ergebnis.
Das fertige Array sieht dann letztendlich so aus. In der rechten unteren Ecke erhalten wir das Ergebnis.

 

Somit haben wir jetzt einen einfachen

Algorithmus zumvergleichen zweier Zeichenketten. Der nächste Schritt ist es diese Funktionauch für den SQL Server zur Verfügung zu stellen. Hierzu erstellen wir mitHilfe von Visual Studio ein „F# Library“-Projekt und fügen dort unseren Algorithmusein. Damit unsere Funktion auch von außen sichtbar ist, müssen wir Sie in einerKlasse kapseln. Hierfür werden die Schlüsselwörter „type“ bzw. „module“verwendet.

namespace FSharpTestLib
open System

module MyModule =
	… //Hier folgen die Funktionen…

 

Kompilieren wir unsere Anwendung erhalten wir zwei Dateien. Neben einer DLL-Datei mit dem Namen unseres F#-Projekts wurde zudem eine Datei mit dem Namen „FSharp.Core.dll“ erstellt. Die „FSharp.Core.dll“ enthält F# spezifische .NET-Klassen die zusätzlich eingebunden werden müssen, um die Funktionsweise unserer DLL sicherzustellen.

 

Der letzte Schritt besteht darin unsere erstellte Funktion im SQL Server einzubinden. Hierfür kopieren wir die beiden DLL-Dateien in ein für den SQL Server zugreifbares Verzeichnis. Anschließend führen wir folgende SQL-Statements aus.

/*** Nutzung externer Programmbibliotheken erlauben ***/
sp_configure 'clr enabled', 1
GO
reconfigure
GO

/*** Zur Vereinfachung schalten wir Verschlüsselung der externen Programmbibliotheken aus ***/
alter database DLLTest set trustworthy on

/*** Einbinden der zusätzlich benötigten .NET-Klassen ***/
CREATE ASSEMBLY FSharpCore
FROM 'D:\Temp\FSharp.Core.dll'
WITH PERMISSION_SET = UNSAFE;

/*** Einbinden der von uns erstellten DLL ***/
CREATE ASSEMBLY FSharpTestLib 
FROM 'D:\Temp\FSharpTestLib.dll'
WITH PERMISSION_SET = UNSAFE;

GO

/*** Erstellen einer SQL-Funktion ***/
CREATE FUNCTION dbo.levenshtein 
(
	@s1 nvarchar(max),
	@s2 nvarchar(max)
)
RETURNS int
AS EXTERNAL NAME FsharpTestLib.[FSharpTestLib.MyModule].levenshtein

GO

/*** Mit der Funktion kann nun der Levenshtein-Algorithmus im SQL verwendet werden ***/
SELECT [dbo].[levenshtein]('Weihnachtsmarkt','Weihnachtsbaum')
--> Ergebnis: 4

 

Mit „sp_configure“ erlauben wir die Nutzung externer Programmbibliotheken. Anschließend können wir unsere DLL als Assembly einbinden und in Form einer Funktion schließlich im SQL verwenden. Der Einfachheit halber wurde in diesem Beispiel auf eine Verschlüsselung der Assemblies verzichtet.

 

Zum Abschluss nun noch ein kleines SQL-Beispiel zur Demonstration der Funktionsweise. Abgefragt wird hier eine einspaltige Tabelle mit Suchwörtern. Das CASE-Konstrukt dient dazu, die längere der zu vergleichenden Zeichenketten als Grundlage der prozentualen Berechnung zu verwenden.

SELECT 
suchstring, 
[dbo].[levenshtein]('Weihnachtsbaum', suchstring) AS LevenshteinDistanz, 
CONVERT(float,1-([dbo].[levenshtein]('Weihnachtsbaum', suchstring)/CONVERT(float,CASE WHEN
												  LEN(suchstring) > LEN('Weihnachtsbaum') 
											      THEN
												  LEN(suchstring)
											      ELSE
												  LEN('Weihnachtsbaum')
											      END))) * 100 AS [Übereinstimmung in %]
FROM [dbo].[DLLTest]
ORDER by LevenshteinDistanz

 

 

Fazit: Wie in diesem Blogbeitrag gezeigt, lässt sich der Funktionsumfang der .NET Sprachen relativ einfach im SQL Server verwenden. Dadurch lassen sich Probleme lösen, die ansonsten aufwendige SQL Lösungen erfordern würden. Dabei können neben F# auch andere .NET Sprachen verwendet werden.

2 Antworten auf „SQL Server: Erweiterung des Funktionsumfangs mittels F# Libraries“

    1. Hallo Jörg,

      vielen Dank für den Link! Dieser T-SQL Funktion war ich mir bisher noch nicht bewusst.

      Der hier vorgestellte Algorithmus sollte auch nur als relativ einfaches Beispiel dienen und lässt sich auf jeden Fall noch an der einen oder anderen Stelle optimieren. Ziel war es vor allem die Möglichkeit der Einbindung von .NET Libraries zu demonstrieren.

      Grüße
      Sebastian

Schreibe einen Kommentar