Database
 sql >> Datenbank >  >> RDS >> Database

Nächstes Spiel, Teil 2

Letzten Monat habe ich ein Rätsel behandelt, bei dem jede Zeile aus einer Tabelle mit der besten Übereinstimmung aus einer anderen Tabelle abgeglichen wurde. Ich habe dieses Rätsel von Karen Ly, einer Jr. Fixed Income Analystin bei RBC, bekommen. Ich habe zwei relationale Hauptlösungen behandelt, die den APPLY-Operator mit TOP-basierten Unterabfragen kombiniert haben. Lösung 1 hatte immer eine quadratische Skalierung. Lösung 2 war recht gut, wenn sie mit guten unterstützenden Indizes versehen war, aber ohne diese Indizes hatte sie auch quadrische Skalierung. In diesem Artikel behandle ich iterative Lösungen, die zwar von SQL-Profis allgemein verpönt sind, in unserem Fall aber auch ohne optimale Indizierung eine viel bessere Skalierung bieten.

Die Herausforderung

Zur Erinnerung:Unsere Herausforderung umfasst Tabellen namens T1 und T2, die Sie mit dem folgenden Code erstellen:

  SET NOCOUNT ON;
 
  IF DB_ID('testdb') IS NULL
    CREATE DATABASE testdb;
  GO
 
  USE testdb;
 
  DROP TABLE IF EXISTS dbo.T1, dbo.T2;
 
  CREATE TABLE dbo.T1
  (
    keycol INT NOT NULL IDENTITY
      CONSTRAINT PK_T1 PRIMARY KEY,
    val INT NOT NULL,
    othercols BINARY(100) NOT NULL
      CONSTRAINT DFT_T1_col1 DEFAULT(0xAA)
  );
 
  CREATE TABLE dbo.T2
  (
    keycol INT NOT NULL IDENTITY
      CONSTRAINT PK_T2 PRIMARY KEY,
    val INT NOT NULL,
    othercols BINARY(100) NOT NULL
      CONSTRAINT DFT_T2_col1 DEFAULT(0xBB)
  );

Sie verwenden dann den folgenden Code, um die Tabellen mit kleinen Sätzen von Beispieldaten zu füllen, um die Korrektheit Ihrer Lösungen zu überprüfen:

  TRUNCATE TABLE dbo.T1;
  TRUNCATE TABLE dbo.T2;
 
  INSERT INTO dbo.T1 (val)
    VALUES(1),(1),(3),(3),(5),(8),(13),(16),(18),(20),(21);
 
  INSERT INTO dbo.T2 (val)
    VALUES(2),(2),(7),(3),(3),(11),(11),(13),(17),(19);

Erinnern Sie sich, die Herausforderung bestand darin, jeder Zeile von T1 die Zeile von T2 zuzuordnen, in der die absolute Differenz zwischen T2.val und T1.val am geringsten ist. Im Falle von Unentschieden sollten Sie val aufsteigend, keycol aufsteigend als Tiebreaker verwenden.

Hier ist das gewünschte Ergebnis für die angegebenen Beispieldaten:

  keycol1     val1        othercols1 keycol2     val2        othercols2
  ----------- ----------- ---------- ----------- ----------- ----------
  1           1           0xAA       1           2           0xBB
  2           1           0xAA       1           2           0xBB
  3           3           0xAA       4           3           0xBB
  4           3           0xAA       4           3           0xBB
  5           5           0xAA       4           3           0xBB
  6           8           0xAA       3           7           0xBB
  7           13          0xAA       8           13          0xBB
  8           16          0xAA       9           17          0xBB
  9           18          0xAA       9           17          0xBB
  10          20          0xAA       10          19          0xBB
  11          21          0xAA       10          19          0xBB

Um die Leistungsfähigkeit Ihrer Lösungen zu überprüfen, benötigen Sie größere Beispieldatensätze. Sie erstellen zuerst die Hilfsfunktion GetNums, die eine Folge von Ganzzahlen in einem angeforderten Bereich generiert, indem Sie den folgenden Code verwenden:

  DROP FUNCTION IF EXISTS dbo.GetNums;
  GO
 
  CREATE OR ALTER FUNCTION dbo.GetNums(@low AS BIGINT, @high AS BIGINT) RETURNS TABLE
  AS
  RETURN
    WITH
      L0   AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)),
      L1   AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
      L2   AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
      L3   AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
      L4   AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),
      L5   AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),
      Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
               FROM L5)
    SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
    FROM Nums
    ORDER BY rownum;
  GO

Dann füllen Sie T1 und T2 mit dem folgenden Code und passen die Parameter an, die die Anzahl der Zeilen und Maximalwerte basierend auf Ihren Anforderungen angeben:

  DECLARE
    @numrowsT1 AS INT = 1000000,
    @maxvalT1  AS INT = 10000000,
    @numrowsT2 AS INT = 1000000,
    @maxvalT2  AS INT = 10000000;
 
  TRUNCATE TABLE dbo.T1;
  TRUNCATE TABLE dbo.T2;
 
  INSERT INTO dbo.T1 WITH(TABLOCK) (val)
    SELECT ABS(CHECKSUM(NEWID())) % @maxvalT1 + 1 AS val
    FROM dbo.GetNums(1, @numrowsT1) AS Nums;
 
  INSERT INTO dbo.T2 WITH(TABLOCK) (val)
    SELECT ABS(CHECKSUM(NEWID())) % @maxvalT2 + 1 AS val
    FROM dbo.GetNums(1, @numrowsT2) AS Nums;

In diesem Beispiel füllen Sie die Tabellen mit jeweils 1.000.000 Zeilen, mit Werten im Bereich von 1 bis 10.000.000 in der val-Spalte (niedrige Dichte).

Lösung 3, Verwendung eines Cursors und einer festplattenbasierten Tabellenvariablen

Eine effiziente iterative Lösung für unsere Herausforderung der engsten Übereinstimmung basiert auf einem Algorithmus, der dem Merge-Join-Algorithmus ähnlich ist. Die Idee ist, nur einen geordneten Pass gegen jeden Tisch mit Cursorn anzuwenden, die Ordnungs- und Tiebreak-Elemente in jeder Runde zu bewerten, um zu entscheiden, auf welcher Seite man vorrückt, und die Reihen auf dem Weg abzugleichen.

Der geordnete Durchlauf für jede Tabelle wird sicherlich von unterstützenden Indizes profitieren, aber die Implikation, wenn diese nicht vorhanden sind, ist, dass eine explizite Sortierung stattfindet. Das bedeutet, dass der Sortierteil eine n-log-n-Skalierung erfordert, aber das ist viel weniger schwerwiegend als die quadratische Skalierung, die Sie unter ähnlichen Umständen aus Lösung 2 erhalten.

Auch die Leistung der Lösungen 1 und 2 wurde durch die Dichte der Val-Säule beeinflusst. Bei höherer Dichte wendete der Plan weniger Neubindungen an. Da umgekehrt die iterativen Lösungen nur einen Durchlauf für jede der Eingaben durchführen, ist die Dichte der val-Spalte kein leistungsbeeinflussender Faktor.

Verwenden Sie den folgenden Code, um unterstützende Indizes zu erstellen:

  CREATE INDEX idx_val_key ON dbo.T1(val, keycol) INCLUDE(othercols);
  CREATE INDEX idx_val_key ON dbo.T2(val, keycol) INCLUDE(othercols);

Stellen Sie sicher, dass Sie die Lösungen sowohl mit als auch ohne diese Indizes testen.

Hier ist der vollständige Code für Lösung 3:

  SET NOCOUNT ON;
 
  BEGIN TRAN;
 
  DECLARE
    @keycol1 AS INT, @val1 AS INT, @othercols1 AS BINARY(100),
    @keycol2 AS INT, @val2 AS INT, @othercols2 AS BINARY(100),
    @prevkeycol2 AS INT, @prevval2 AS INT, @prevothercols2 AS BINARY(100),
    @C1 AS CURSOR, @C2 AS CURSOR,
    @C1fetch_status AS INT, @C2fetch_status AS INT;
 
  DECLARE @Result AS TABLE
  (
    keycol1    INT         NOT NULL PRIMARY KEY,
    val1       INT         NOT NULL,
    othercols1 BINARY(100) NOT NULL,
    keycol2    INT         NULL,
    val2       INT         NULL,
    othercols2 BINARY(100) NULL
  );
 
  SET @C1 = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
    SELECT keycol, val, othercols FROM dbo.T1 ORDER BY val, keycol;
 
  SET @C2 = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
    SELECT keycol, val, othercols FROM dbo.T2 ORDER BY val, keycol;
 
  OPEN @C1;
  OPEN @C2;
 
  FETCH NEXT FROM @C2 INTO @keycol2, @val2, @othercols2;
 
  SET @C2fetch_status = @@fetch_status;
 
  SELECT @prevkeycol2 = @keycol2, @prevval2 = @val2, @prevothercols2 = @othercols2;
 
  FETCH NEXT FROM @C1 INTO @keycol1, @val1, @othercols1;
 
  SET @C1fetch_status = @@fetch_status;
 
  WHILE @C1fetch_status = 0
  BEGIN
    IF @val1 <= @val2 OR @C2fetch_status <> 0
    BEGIN
      IF ABS(@val1 - @val2) < ABS(@val1 - @prevval2)
        INSERT INTO @Result(keycol1, val1, othercols1, keycol2, val2, othercols2)
          VALUES(@keycol1, @val1, @othercols1, @keycol2, @val2, @othercols2);
      ELSE
        INSERT INTO @Result(keycol1, val1, othercols1, keycol2, val2, othercols2)
          VALUES(@keycol1, @val1, @othercols1, @prevkeycol2, @prevval2, @prevothercols2);
 
      FETCH NEXT FROM @C1 INTO @keycol1, @val1, @othercols1;
      SET @C1fetch_status = @@fetch_status;
    END
    ELSE IF @C2fetch_status = 0
    BEGIN
      IF @val2 > @prevval2
        SELECT @prevkeycol2 = @keycol2, @prevval2 = @val2, @prevothercols2 = @othercols2;
 
      FETCH NEXT FROM @C2 INTO @keycol2, @val2, @othercols2;
      SET @C2fetch_status = @@fetch_status;
    END;  
  END;
 
  SELECT
     keycol1, val1, SUBSTRING(othercols1, 1, 1) AS othercols1,
     keycol2, val2, SUBSTRING(othercols2, 1, 1) AS othercols2
  FROM @Result;
 
  COMMIT TRAN;

Der Code verwendet eine Tabellenvariable namens @Result, um die Übereinstimmungen zu speichern, und gibt sie schließlich zurück, indem er die Tabellenvariable abfragt. Beachten Sie, dass der Code die Arbeit in einer Transaktion ausführt, um die Protokollierung zu reduzieren.

Der Code verwendet Cursor-Variablen namens @C1 und @C2, um Zeilen in T1 bzw. T2 zu durchlaufen, in beiden Fällen geordnet nach val, keycol. Lokale Variablen werden verwendet, um die aktuellen Zeilenwerte von jedem Cursor zu speichern (@keycol1, @val1 und @othercols1 für @C1 und @keycol2, @val2 und @othercols2 für @C2). Zusätzliche lokale Variablen speichern die vorherigen Zeilenwerte von @C2 (@prevkeycol2, @prevval2 und @prevothercols2). Die Variablen @C1fetch_status und @C2fetch_status enthalten den Status des letzten Abrufs vom jeweiligen Cursor.

Nach dem Deklarieren und Öffnen beider Cursor ruft der Code eine Zeile von jedem Cursor in die entsprechenden lokalen Variablen ab und speichert zunächst die aktuellen Zeilenwerte von @C2 auch in den vorherigen Zeilenvariablen. Der Code tritt dann in eine Schleife ein, die weiterläuft, während der letzte Abruf von @C1 erfolgreich war (@C1fetch_status =0). Der Rumpf der Schleife wendet in jeder Runde den folgenden Pseudocode an:

  If @val1 <= @val2 or reached end of @C2

  Begin

    If absolute difference between @val1 and @val2 is less than between @val1 and @prevval2

      Add row to @Result with current row values from @C1 and current row values from @C2

    Else

      Add row to @Result with current row values from @C1 and previous row values from @C2

    Fetch next row from @C1

  End

  Else if last fetch from @C2 was successful

  Begin

    If @val2 > @prevval2

      Set variables holding @C2’s previous row values to values of current row variables

    Fetch next row from @C2

  End

Der Code fragt dann einfach die Tabellenvariable @Result ab, um alle Übereinstimmungen zurückzugeben.

Unter Verwendung der großen Sätze von Beispieldaten (1.000.000 Zeilen in jeder Tabelle) mit optimaler Indizierung dauerte es auf meinem System 38 Sekunden, bis diese Lösung abgeschlossen war, und führte 28.240 logische Lesevorgänge durch. Die Skalierung dieser Lösung ist dann natürlich linear. Ohne optimale Indizierung dauerte es 40 Sekunden (nur 2 Sekunden extra!) und führte 29.519 logische Lesevorgänge durch. Der Sortierteil in dieser Lösung hat eine Skalierung von n log n.

Lösung 4, Verwendung eines Cursors und einer speicheroptimierten Tabellenvariablen

Um die Leistung des iterativen Ansatzes zu verbessern, könnten Sie versuchen, die Verwendung der datenträgerbasierten Tabellenvariablen durch eine speicheroptimierte zu ersetzen. Da die Lösung darin besteht, 1.000.000 Zeilen in die Tabellenvariable zu schreiben, könnte dies zu einer nicht zu vernachlässigenden Verbesserung führen.

Zuerst müssen Sie In-Memory OLTP in der Datenbank aktivieren, indem Sie eine Dateigruppe erstellen, die als CONTAINS MEMORY_OPTIMIZED_DATA gekennzeichnet ist, und darin einen Container, der auf einen Ordner im Dateisystem verweist. Angenommen, Sie haben zuvor einen übergeordneten Ordner mit dem Namen C:\IMOLTP\ erstellt, verwenden Sie den folgenden Code, um diese beiden Schritte anzuwenden:

  ALTER DATABASE testdb
    ADD FILEGROUP testdb_MO CONTAINS MEMORY_OPTIMIZED_DATA;
 
  ALTER DATABASE testdb
    ADD FILE ( NAME = testdb_dir,
               FILENAME = 'C:\IMOLTP\testdb_dir' )
      TO FILEGROUP testdb_MO;

Der nächste Schritt besteht darin, einen speicheroptimierten Tabellentyp als Vorlage für unsere Tabellenvariable zu erstellen, indem Sie den folgenden Code ausführen:

  DROP TYPE IF EXISTS dbo.TYPE_closestmatch;
  GO
 
  CREATE TYPE dbo.TYPE_closestmatch AS TABLE
  (
    keycol1    INT         NOT NULL PRIMARY KEY NONCLUSTERED,
    val1       INT         NOT NULL,
    othercols1 BINARY(100) NOT NULL,
    keycol2    INT         NULL,
    val2       INT         NULL,
    othercols2 BINARY(100) NULL
  )
  WITH (MEMORY_OPTIMIZED = ON);

Dann würden Sie anstelle der ursprünglichen Deklaration der Tabellenvariable @Result den folgenden Code verwenden:

  DECLARE @Result AS dbo.TYPE_closestmatch;

Hier ist der vollständige Lösungscode:

  SET NOCOUNT ON;
 
  USE testdb;
 
  BEGIN TRAN;
 
  DECLARE
    @keycol1 AS INT, @val1 AS INT, @othercols1 AS BINARY(100),
    @keycol2 AS INT, @val2 AS INT, @othercols2 AS BINARY(100),
    @prevkeycol2 AS INT, @prevval2 AS INT, @prevothercols2 AS BINARY(100),
    @C1 AS CURSOR, @C2 AS CURSOR,
    @C1fetch_status AS INT, @C2fetch_status AS INT;
 
  DECLARE @Result AS dbo.TYPE_closestmatch;
 
  SET @C1 = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
    SELECT keycol, val, othercols FROM dbo.T1 ORDER BY val, keycol;
 
  SET @C2 = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
    SELECT keycol, val, othercols FROM dbo.T2 ORDER BY val, keycol;
 
  OPEN @C1;
  OPEN @C2;
 
  FETCH NEXT FROM @C2 INTO @keycol2, @val2, @othercols2;
 
  SET @C2fetch_status = @@fetch_status;
 
  SELECT @prevkeycol2 = @keycol2, @prevval2 = @val2, @prevothercols2 = @othercols2;
 
  FETCH NEXT FROM @C1 INTO @keycol1, @val1, @othercols1;
 
  SET @C1fetch_status = @@fetch_status;
 
  WHILE @C1fetch_status = 0
  BEGIN
    IF @val1 <= @val2 OR @C2fetch_status <> 0
    BEGIN
      IF ABS(@val1 - @val2) < ABS(@val1 - @prevval2)
        INSERT INTO @Result(keycol1, val1, othercols1, keycol2, val2, othercols2)
          VALUES(@keycol1, @val1, @othercols1, @keycol2, @val2, @othercols2);
      ELSE
        INSERT INTO @Result(keycol1, val1, othercols1, keycol2, val2, othercols2)
          VALUES(@keycol1, @val1, @othercols1, @prevkeycol2, @prevval2, @prevothercols2);
 
      FETCH NEXT FROM @C1 INTO @keycol1, @val1, @othercols1;
      SET @C1fetch_status = @@fetch_status;
    END
    ELSE IF @C2fetch_status = 0
    BEGIN
      IF @val2 > @prevval2
        SELECT @prevkeycol2 = @keycol2, @prevval2 = @val2, @prevothercols2 = @othercols2;
 
      FETCH NEXT FROM @C2 INTO @keycol2, @val2, @othercols2;
      SET @C2fetch_status = @@fetch_status;
    END;  
  END;
 
  SELECT
     keycol1, val1, SUBSTRING(othercols1, 1, 1) AS othercols1,
     keycol2, val2, SUBSTRING(othercols2, 1, 1) AS othercols2
  FROM @Result;
 
  COMMIT TRAN;

Mit der optimalen Indizierung dauerte die Fertigstellung dieser Lösung auf meinem Computer 27 Sekunden (im Vergleich zu 38 Sekunden mit der datenträgerbasierten Tabellenvariablen), und ohne optimale Indizierung dauerte sie 29 Sekunden (im Vergleich zu 40 Sekunden). Das ist eine fast 30-prozentige Reduzierung der Laufzeit.

Lösung 5, mit SQL CLR

Eine andere Möglichkeit, die Leistung des iterativen Ansatzes weiter zu verbessern, besteht darin, die Lösung mit SQL CLR zu implementieren, da der größte Teil des Overheads der T-SQL-Lösung auf die Ineffizienzen des Cursor-Abrufs und der Schleifen in T-SQL zurückzuführen ist.

Hier ist der vollständige Lösungscode, der denselben Algorithmus implementiert, den ich in Lösungen 3 und 4 mit C# verwendet habe, wobei SqlDataReader-Objekte anstelle von T-SQL-Cursorn verwendet werden:

  using System;
  using System.Data;
  using System.Data.SqlClient;
  using System.Data.SqlTypes;
  using Microsoft.SqlServer.Server;
 
  public partial class ClosestMatch
  {
      [SqlProcedure]
      public static void GetClosestMatches()
      {
          using (SqlConnection conn = new SqlConnection("data source=MyServer\\MyInstance;Database=testdb;Trusted_Connection=True;MultipleActiveResultSets=true;"))
          {
              SqlCommand comm1 = new SqlCommand();
              SqlCommand comm2 = new SqlCommand();
              comm1.Connection = conn;
              comm2.Connection = conn;
              comm1.CommandText = "SELECT keycol, val, othercols FROM dbo.T1 ORDER BY val, keycol;";
              comm2.CommandText = "SELECT keycol, val, othercols FROM dbo.T2 ORDER BY val, keycol;";
 
              SqlMetaData[] columns = new SqlMetaData[6];
              columns[0] = new SqlMetaData("keycol1", SqlDbType.Int);
              columns[1] = new SqlMetaData("val1", SqlDbType.Int);
              columns[2] = new SqlMetaData("othercols1", SqlDbType.Binary, 100);
              columns[3] = new SqlMetaData("keycol2", SqlDbType.Int);
              columns[4] = new SqlMetaData("val2", SqlDbType.Int);
              columns[5] = new SqlMetaData("othercols2", SqlDbType.Binary, 100);
 
              SqlDataRecord record = new SqlDataRecord(columns);
              SqlContext.Pipe.SendResultsStart(record);
              conn.Open();
              SqlDataReader reader1 = comm1.ExecuteReader();
              SqlDataReader reader2 = comm2.ExecuteReader();
              SqlInt32 keycol1 = SqlInt32.Null;
              SqlInt32 val1 = SqlInt32.Null;
              SqlBinary othercols1 = SqlBinary.Null;
              SqlInt32 keycol2 = SqlInt32.Null;
              SqlInt32 val2 = SqlInt32.Null;
              SqlBinary othercols2 = SqlBinary.Null;
              SqlInt32 prevkeycol2 = SqlInt32.Null;
              SqlInt32 prevval2 = SqlInt32.Null;
              SqlBinary prevothercols2 = SqlBinary.Null;
 
              Boolean reader2foundrow = reader2.Read();
 
              if (reader2foundrow)
              {
                  keycol2 = reader2.GetSqlInt32(0);
                  val2 = reader2.GetSqlInt32(1);
                  othercols2 = reader2.GetSqlBinary(2);
                  prevkeycol2 = keycol2;
                  prevval2 = val2;
                  prevothercols2 = othercols2;
              }
 
              Boolean reader1foundrow = reader1.Read();
 
              if (reader1foundrow)
              {
                  keycol1 = reader1.GetSqlInt32(0);
                  val1 = reader1.GetSqlInt32(1);
                  othercols1 = reader1.GetSqlBinary(2);
              }
 
              while (reader1foundrow)
              {
                  if (val1 <= val2 || !reader2foundrow)
                  {
                      if (Math.Abs((int)(val1 - val2)) < Math.Abs((int)(val1 - prevval2)))
                      {
                          record.SetSqlInt32(0, keycol1);
                          record.SetSqlInt32(1, val1);
                          record.SetSqlBinary(2, othercols1);
                          record.SetSqlInt32(3, keycol2);
                          record.SetSqlInt32(4, val2);
                          record.SetSqlBinary(5, othercols2);
                          SqlContext.Pipe.SendResultsRow(record);
                      }
                      else
                      {
                          record.SetSqlInt32(0, keycol1);
                          record.SetSqlInt32(1, val1);
                          record.SetSqlBinary(2, othercols1);
                          record.SetSqlInt32(3, prevkeycol2);
                          record.SetSqlInt32(4, prevval2);
                          record.SetSqlBinary(5, prevothercols2);
                          SqlContext.Pipe.SendResultsRow(record);                        
                      }
 
                      reader1foundrow = reader1.Read();
 
                      if (reader1foundrow)
                      {
                          keycol1 = reader1.GetSqlInt32(0);
                          val1 = reader1.GetSqlInt32(1);
 
                          othercols1 = reader1.GetSqlBinary(2);
                      }
                  }
                  else if (reader2foundrow)
                  {
                      if (val2 > prevval2)
                      {
                          prevkeycol2 = keycol2;
                          prevval2 = val2;
                          prevothercols2 = othercols2;
                      }
 
                      reader2foundrow = reader2.Read();
 
                      if (reader2foundrow)
                      {                      
                          keycol2 = reader2.GetSqlInt32(0);
                          val2 = reader2.GetSqlInt32(1);
                          othercols2 = reader2.GetSqlBinary(2);
                      }
                  }
              }
              SqlContext.Pipe.SendResultsEnd();
          }
      }
  }

Um sich mit der Datenbank zu verbinden, würden Sie normalerweise die Option "context connection=true" anstelle einer vollständigen Verbindungszeichenfolge verwenden. Leider ist diese Option nicht verfügbar, wenn Sie mit mehreren aktiven Ergebnissätzen arbeiten müssen. Unsere Lösung emuliert die parallele Arbeit mit zwei Cursorn unter Verwendung von zwei SqlDataReader-Objekten, und daher benötigen Sie eine vollständige Verbindungszeichenfolge mit der Option MultipleActiveResultSets=true. Hier ist die vollständige Verbindungszeichenfolge:

  "data source=MyServer\\MyInstance;Database=testdb;Trusted_Connection=True;MultipleActiveResultSets=true;"

In Ihrem Fall müssten Sie natürlich MyServer\\MyInstance durch Ihren Server- und Instanznamen (falls zutreffend) ersetzen.

Auch die Tatsache, dass Sie nicht „context connection=true“ sondern eine explizite Verbindungszeichenfolge verwendet haben, bedeutet, dass die Assembly Zugriff auf eine externe Ressource benötigt und daher vertrauenswürdig ist. Normalerweise würden Sie dies erreichen, indem Sie es mit einem Zertifikat oder einem asymmetrischen Schlüssel signieren, der über eine entsprechende Anmeldung mit der richtigen Berechtigung verfügt, oder es mit der Prozedur sp_add_trusted_assembly auf die Whitelist setzen. Der Einfachheit halber setze ich die Datenbankoption TRUSTWORTHY auf ON und gebe beim Erstellen der Assembly den Berechtigungssatz EXTERNAL_ACCESS an. Der folgende Code stellt die Lösung in der Datenbank bereit:

  EXEC sys.sp_configure 'advanced', 1;
  RECONFIGURE;
 
  EXEC sys.sp_configure 'clr enabled', 1;
  EXEC sys.sp_configure 'clr strict security', 0;
  RECONFIGURE;
 
  EXEC sys.sp_configure 'advanced', 0;
  RECONFIGURE;
 
  ALTER DATABASE testdb SET TRUSTWORTHY ON; 
 
  USE testdb;
 
  DROP PROC IF EXISTS dbo.GetClosestMatches;
 
  DROP ASSEMBLY IF EXISTS ClosestMatch;
 
  CREATE ASSEMBLY ClosestMatch 
    FROM 'C:\ClosestMatch\ClosestMatch\bin\Debug\ClosestMatch.dll'
  WITH PERMISSION_SET = EXTERNAL_ACCESS;
  GO
 
  CREATE PROCEDURE dbo.GetClosestMatches
  AS EXTERNAL NAME ClosestMatch.ClosestMatch.GetClosestMatches;

Der Code aktiviert CLR in der Instanz, deaktiviert die strikte CLR-Sicherheitsoption, setzt die Datenbankoption TRUSTWORTHY auf ON, erstellt die Assembly und erstellt die Prozedur GetClosestMatches.

Verwenden Sie den folgenden Code, um die gespeicherte Prozedur zu testen:

 EXEC dbo.GetClosestMatches;

Die CLR-Lösung dauerte auf meinem System 8 Sekunden mit optimaler Indizierung und 9 Sekunden ohne. Das ist eine ziemlich beeindruckende Leistungssteigerung im Vergleich zu allen anderen Lösungen – sowohl relational als auch iterativ.

Schlussfolgerung

Iterative Lösungen sind in der SQL-Community eher verpönt, da sie nicht dem relationalen Modell folgen. Die Realität ist jedoch, dass Sie manchmal keine leistungsstarken relationalen Lösungen erstellen können und Leistung Priorität hat. Bei einem iterativen Ansatz sind Sie nicht auf die Algorithmen beschränkt, auf die der SQL Server-Optimierer Zugriff hat, sondern können jeden beliebigen Algorithmus implementieren. Wie in diesem Artikel gezeigt, konnten Sie die Aufgabe mit einem Merge-ähnlichen Algorithmus mit einem einzigen geordneten Durchlauf für jede der Eingaben ausführen. Durch die Verwendung von T-SQL-Cursorn und einer festplattenbasierten Tabellenvariablen erhalten Sie eine angemessene Leistung und Skalierung. Durch die Umstellung auf eine speicheroptimierte Tabellenvariable konnten Sie die Leistung um etwa 30 % und durch die Verwendung von SQL CLR um deutlich mehr verbessern.