sql >> Database >  >> RDS >> Database

Dichtstbijzijnde match, deel 2

Vorige maand heb ik een puzzel behandeld waarbij ik elke rij van de ene tafel moest matchen met de dichtstbijzijnde match van een andere tafel. Ik heb deze puzzel gekregen van Karen Ly, een Jr. Fixed Income Analyst bij RBC. Ik heb twee belangrijke relationele oplossingen behandeld die de APPLY-operator combineerden met op TOP gebaseerde subquery's. Oplossing 1 had altijd kwadratische schaling. Oplossing 2 deed het redelijk goed als het werd voorzien van goede ondersteunende indexen, maar zonder die indexen had het ook quadric scaling. In dit artikel bespreek ik iteratieve oplossingen, die ondanks dat ze over het algemeen worden afgekeurd door SQL-professionals, in ons geval een veel betere schaling bieden, zelfs zonder optimale indexering.

De uitdaging

Ter herinnering:onze uitdaging omvat tabellen genaamd T1 en T2, die u maakt met de volgende code:

  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)
  );

Vervolgens gebruikt u de volgende code om de tabellen te vullen met kleine reeksen voorbeeldgegevens om de juistheid van uw oplossingen te controleren:

  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);

Bedenk dat de uitdaging was om voor elke rij van T1 de rij van T2 te matchen waar het absolute verschil tussen T2.val en T1.val het laagst is. In het geval van gelijkspel moet je val oplopend, keycol oplopende volgorde gebruiken als de tiebreaker.

Dit is het gewenste resultaat voor de gegeven voorbeeldgegevens:

  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

Om de prestaties van uw oplossingen te controleren, hebt u grotere sets met voorbeeldgegevens nodig. U maakt eerst de helperfunctie GetNums, die een reeks gehele getallen in een aangevraagd bereik genereert, met behulp van de volgende code:

  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

Vervolgens vult u T1 en T2 in met behulp van de volgende code, waarbij u de parameters aanpast die het aantal rijen en maximale waarden aangeven op basis van uw behoeften:

  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 dit voorbeeld vult u de tabellen met elk 1.000.000 rijen, met waarden in het bereik van 1 - 10.000.000 in de val-kolom (lage dichtheid).

Oplossing 3, met een cursor en een schijfgebaseerde tabelvariabele

Een efficiënte iteratieve oplossing voor onze uitdaging voor de beste match is gebaseerd op een algoritme dat vergelijkbaar is met het samenvoegingsalgoritme. Het idee is om slechts één geordende pas op elke tafel toe te passen met behulp van cursors, de volgorde- en tiebreak-elementen in elke ronde te evalueren om te beslissen aan welke kant je wilt doorgaan, en de rijen onderweg te matchen.

De geordende pas tegen elke tafel zal zeker profiteren van ondersteunende indexen, maar de implicatie van het niet hebben van die indexen is dat expliciete sortering zal plaatsvinden. Dit betekent dat het sorteergedeelte log n schaling met zich meebrengt, maar dat is veel minder ernstig dan de kwadratische schaling die u in vergelijkbare omstandigheden van Oplossing 2 krijgt.

Ook werden de prestaties van oplossingen 1 en 2 beïnvloed door de dichtheid van de val-kolom. Met een hogere dichtheid paste het plan minder rebinds toe. Omgekeerd, aangezien de iteratieve oplossingen slechts één doorgang uitvoeren tegen elk van de ingangen, is de dichtheid van de val-kolom geen prestatiebeïnvloedende factor.

Gebruik de volgende code om ondersteunende indexen te maken:

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

Zorg ervoor dat u de oplossingen zowel met als zonder deze indexen test.

Hier is de volledige code voor oplossing 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;

De code gebruikt een tabelvariabele met de naam @Result om de overeenkomsten op te slaan en retourneert deze uiteindelijk door de tabelvariabele op te vragen. Merk op dat de code het werk in één transactie uitvoert om logboekregistratie te verminderen.

De code gebruikt cursorvariabelen genaamd @C1 en @C2 om door rijen in respectievelijk T1 en T2 te doorlopen, in beide gevallen gerangschikt op val, keycol. Lokale variabelen worden gebruikt om de huidige rijwaarden van elke cursor op te slaan (@keycol1, @val1 en @othercols1 voor @C1 en @keycol2, @val2 en @othercols2 voor @C2). Aanvullende lokale variabelen slaan de vorige rijwaarden van @C2 (@prevkeycol2, @prevval2 en @prevothercols2) op. De variabelen @C1fetch_status en @C2fetch_status houden de status van de laatste ophaalactie van de respectieve cursor vast.

Nadat beide cursors zijn gedeclareerd en geopend, haalt de code een rij van elke cursor op in de respectieve lokale variabelen en slaat aanvankelijk de huidige rijwaarden van @C2 ook op in de vorige rijvariabelen. De code komt dan in een lus die blijft lopen terwijl de laatste fetch van @C1 succesvol was (@C1fetch_status =0). Het lichaam van de lus past de volgende pseudo-code toe in elke ronde:

  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

De code bevraagt ​​vervolgens eenvoudig de tabelvariabele @Result om alle overeenkomsten te retourneren.

Met behulp van de grote sets voorbeeldgegevens (1.000.000 rijen in elke tabel), met optimale indexering, duurde het 38 seconden om deze oplossing op mijn systeem te voltooien en 28.240 logische uitlezingen uit te voeren. De schaal van deze oplossing is dan natuurlijk lineair. Zonder optimale indexering duurde het 40 seconden om te voltooien (slechts 2 seconden extra!), en voerde het 29.519 logische uitlezingen uit. Het sorteergedeelte in deze oplossing heeft n log n schaling.

Oplossing 4, met behulp van een cursor en een voor het geheugen geoptimaliseerde tabelvariabele

In een poging om de prestaties van de iteratieve benadering te verbeteren, zou je kunnen proberen om het gebruik van de schijfgebaseerde tabelvariabele te vervangen door een voor geheugen geoptimaliseerd exemplaar. Aangezien de oplossing erin bestaat 1.000.000 rijen naar de tabelvariabele te schrijven, kan dit resulteren in een niet te verwaarlozen verbetering.

Eerst moet u In-Memory OLTP in de database inschakelen door een bestandsgroep te maken die is gemarkeerd als CONTAINS MEMORY_OPTIMIZED_DATA, en daarin een container die verwijst naar een map in het bestandssysteem. Ervan uitgaande dat u vooraf een bovenliggende map met de naam C:\IMOLTP\ hebt gemaakt, gebruikt u de volgende code om deze twee stappen toe te passen:

  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;

De volgende stap is het maken van een voor geheugen geoptimaliseerd tabeltype als sjabloon voor onze tabelvariabele door de volgende code uit te voeren:

  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);

Dan zou je in plaats van de originele declaratie van de tabelvariabele @Result de volgende code gebruiken:

  DECLARE @Result AS dbo.TYPE_closestmatch;

Hier is de volledige oplossingscode:

  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;

Met de optimale indexering duurde het 27 seconden om deze oplossing op mijn computer te voltooien (vergeleken met 38 seconden met de schijfgebaseerde tabelvariabele), en zonder optimale indexering duurde het 29 seconden om te voltooien (vergeleken met 40 seconden). Dat is bijna 30 procent reductie in de looptijd.

Oplossing 5, met SQL CLR

Een andere manier om de prestaties van de iteratieve benadering verder te verbeteren, is door de oplossing te implementeren met SQL CLR, aangezien de meeste overhead van de T-SQL-oplossing te wijten is aan de inefficiëntie van het ophalen en herhalen van de cursor in T-SQL.

Hier is de volledige oplossingscode die hetzelfde algoritme implementeert dat ik gebruikte in Oplossingen 3 en 4 met C#, met SqlDataReader-objecten in plaats van T-SQL-cursors:

  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();
          }
      }
  }

Om verbinding te maken met de database zou u normaal gesproken de optie "context connection=true" gebruiken in plaats van een volledige verbindingsreeks. Helaas is deze optie niet beschikbaar wanneer u met meerdere actieve resultatensets moet werken. Onze oplossing emuleert parallel werk met twee cursors met behulp van twee SqlDataReader-objecten, en daarom hebt u een volledige verbindingsreeks nodig, met de optie MultipleActiveResultSets=true. Hier is de volledige verbindingsreeks:

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

Natuurlijk moet u in uw geval MyServer\\MyInstance vervangen door uw server- en instantienamen (indien relevant).

Ook betekent het feit dat u "context connection=true" niet hebt gebruikt in plaats van een expliciete verbindingsreeks, dat de assembly toegang nodig heeft tot een externe bron en daarom moet worden vertrouwd. Normaal gesproken zou u dit bereiken door het te ondertekenen met een certificaat of een asymmetrische sleutel met een bijbehorende login met de juiste toestemming, of door het op de witte lijst te zetten met behulp van de sp_add_trusted_assembly procedure. Voor de eenvoud zal ik de database-optie TRUSTWORTHY op ON zetten en de EXTERNAL_ACCESS-machtigingenset specificeren bij het maken van de assembly. De volgende code implementeert de oplossing in de database:

  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;

De code schakelt CLR in de instantie in, schakelt de strikte CLR-beveiligingsoptie uit, stelt de databaseoptie TRUSTWORTHY in op ON, maakt de assembly en maakt de procedure GetClosestMatches aan.

Gebruik de volgende code om de opgeslagen procedure te testen:

 EXEC dbo.GetClosestMatches;

De CLR-oplossing duurde 8 seconden om te voltooien op mijn systeem met optimale indexering en 9 seconden zonder. Dat is een behoorlijk indrukwekkende prestatieverbetering in vergelijking met alle andere oplossingen — zowel relationeel als iteratief.

Conclusie

Iteratieve oplossingen worden typisch afgekeurd in de SQL-gemeenschap, omdat ze niet het relationele model volgen. De realiteit is echter dat u soms niet in staat bent om goed presterende relationele oplossingen te creëren en dat prestaties een prioriteit zijn. Door een iteratieve benadering te gebruiken, bent u niet beperkt tot de algoritmen waartoe de SQL Server-optimizer toegang heeft, maar kunt u elk willekeurig algoritme implementeren. Zoals in dit artikel wordt aangetoond, kon je met behulp van een samenvoeg-achtig algoritme de taak bereiken met een enkele geordende pass tegen elk van de ingangen. Met behulp van T-SQL-cursors en een schijfgebaseerde tabelvariabele kreeg je redelijke prestaties en schaalbaarheid. U kon de prestaties met ongeveer 30 procent verbeteren door over te schakelen naar een tabelvariabele met geoptimaliseerd geheugen, en aanzienlijk meer door SQL CLR te gebruiken.


  1. Database hernoemen in SQL Server - SQL Server / TSQL-zelfstudie, deel 26

  2. Oracle INSERT in twee tabellen in één query

  3. Hoe verouderde oracle.sql.ArrayDescriptor, oracle.sql.STRUCT en oracle.sql.StructDescriptor te repareren

  4. Krijg n gegroepeerde categorieën en tel anderen op tot één