W zeszłym miesiącu omówiłem zagadkę polegającą na dopasowaniu każdego wiersza z jednego stołu do najbliższego dopasowania z innego stołu. Dostałem tę zagadkę od Karen Ly, Jr. Analityka Stałego Dochodu w RBC. Omówiłem dwa główne rozwiązania relacyjne, które łączyły operator APPLY z podzapytaniami opartymi na TOP. Rozwiązanie 1 zawsze miało skalowanie kwadratowe. Rozwiązanie 2 radziło sobie całkiem dobrze, gdy było wyposażone w dobre indeksy pomocnicze, ale bez tych indeksów również miało skalowanie kwadratowe. W tym artykule zajmę się rozwiązaniami iteracyjnymi, które pomimo tego, że generalnie nie podobają się profesjonalistom SQL, zapewniają znacznie lepsze skalowanie w naszym przypadku nawet bez optymalnego indeksowania.
Wyzwanie
Przypominamy, że nasze wyzwanie obejmuje tabele o nazwach T1 i T2, które tworzysz za pomocą następującego kodu:
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) );
Następnie użyj poniższego kodu, aby wypełnić tabele małymi zestawami przykładowych danych w celu sprawdzenia poprawności rozwiązań:
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);
Przypomnijmy, wyzwanie polegało na dopasowaniu do każdego wiersza z T1 tego wiersza z T2, gdzie bezwzględna różnica między T2.val i T1.val jest najniższa. W przypadku remisów, jako rozstrzygacza remisów należy używać wartości val ascending, keycol rosnącej kolejności.
Oto pożądany wynik dla podanych przykładowych danych:
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
Aby sprawdzić wydajność swoich rozwiązań, potrzebujesz większych zestawów przykładowych danych. Najpierw tworzysz funkcję pomocniczą GetNums, która generuje sekwencję liczb całkowitych w żądanym zakresie, używając następującego kodu:
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
Następnie wypełniasz T1 i T2 za pomocą następującego kodu, dostosowując parametry wskazujące liczbę wierszy i maksymalne wartości w zależności od potrzeb:
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;
W tym przykładzie wypełniasz tabele po 1 000 000 wierszy każda, wartościami z zakresu od 1 do 10 000 000 w kolumnie val (niska gęstość).
Rozwiązanie 3, użycie kursora i zmiennej tabeli opartej na dysku
Wydajne rozwiązanie iteracyjne dla naszego wyzwania polegającego na najbliższym dopasowaniu jest oparte na algorytmie podobnym do algorytmu łączenia łączenia. Chodzi o to, aby zastosować tylko jedno uporządkowane podanie do każdej tabeli za pomocą kursorów, oceniając elementy kolejności i rozstrzygania w każdej rundzie, aby zdecydować, po której stronie przejść, i dopasowując wiersze po drodze.
Uporządkowane przejście względem każdej tabeli z pewnością skorzysta na wspieraniu indeksów, ale implikacją ich braku jest to, że nastąpi jawne sortowanie. Oznacza to, że część sortująca będzie podlegać skalowaniu n log n, ale jest to znacznie mniej dotkliwe niż skalowanie kwadratowe, które uzyskuje się z Rozwiązania 2 w podobnych okolicznościach.
Również na wydajność Roztworów 1 i 2 miała wpływ gęstość kolumny val. Przy większej gęstości plan zakładał mniejszą liczbę ponownych wiązań. I odwrotnie, ponieważ rozwiązania iteracyjne wykonują tylko jedno przejście dla każdego z danych wejściowych, gęstość kolumny val nie jest czynnikiem wpływającym na wydajność.
Użyj następującego kodu, aby utworzyć indeksy pomocnicze:
CREATE INDEX idx_val_key ON dbo.T1(val, keycol) INCLUDE(othercols); CREATE INDEX idx_val_key ON dbo.T2(val, keycol) INCLUDE(othercols);
Upewnij się, że testujesz rozwiązania zarówno z tymi indeksami, jak i bez nich.
Oto kompletny kod rozwiązania 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;
Kod używa zmiennej tabeli o nazwie @Result do przechowywania dopasowań i ostatecznie zwraca je, wysyłając zapytanie do zmiennej tabeli. Zauważ, że kod wykonuje pracę w jednej transakcji, aby zmniejszyć rejestrowanie.
Kod używa zmiennych kursora o nazwie @C1 i @C2 do iteracji przez wiersze odpowiednio w T1 i T2, w obu przypadkach uporządkowanych według val, keycol. Zmienne lokalne służą do przechowywania bieżących wartości wierszy z każdego kursora (@keycol1, @val1 i @othercols1 dla @C1 i @keycol2, @val2 i @othercols2 dla @C2). Dodatkowe zmienne lokalne przechowują wartości poprzedniego wiersza z @C2 (@prevkeycol2, @prevval2 i @prevothercols2). Zmienne @C1fetch_status i @C2fetch_status przechowują status ostatniego pobrania z odpowiedniego kursora.
Po zadeklarowaniu i otwarciu obu kursorów kod pobiera wiersz z każdego kursora do odpowiednich zmiennych lokalnych i początkowo przechowuje bieżące wartości wierszy z @C2 również w poprzednich zmiennych wiersza. Następnie kod wprowadza pętlę, która działa, gdy ostatnie pobieranie z @C1 zakończyło się powodzeniem (@C1fetch_status =0). Ciało pętli stosuje następujący pseudokod w każdej rundzie:
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
Następnie kod po prostu wysyła zapytanie do zmiennej tabeli @Result, aby zwrócić wszystkie dopasowania.
Korzystając z dużych zestawów przykładowych danych (1.000.000 wierszy w każdej tabeli), z optymalnym indeksowaniem, wykonanie tego rozwiązania w moim systemie zajęło 38 sekund i wykonało 28 240 odczytów logicznych. Oczywiście skalowanie tego rozwiązania jest wtedy liniowe. Bez optymalnego indeksowania ukończenie zajęło 40 sekund (tylko 2 sekundy więcej!) i wykonało 29 519 odczytów logicznych. Część sortująca w tym rozwiązaniu ma skalowanie n log n.
Rozwiązanie 4, użycie kursora i zmiennej tabeli zoptymalizowanej pod kątem pamięci
Próbując poprawić wydajność podejścia iteracyjnego, można spróbować zastąpić użycie zmiennej tabeli opartej na dysku na zoptymalizowaną pod kątem pamięci. Ponieważ rozwiązanie polega na zapisaniu 1 000 000 wierszy do zmiennej tabeli, może to spowodować nieistotną poprawę.
Najpierw należy włączyć protokół OLTP w pamięci w bazie danych, tworząc grupę plików oznaczoną jako CONTAINS MEMORY_OPTIMIZED_DATA, a w niej kontener wskazujący folder w systemie plików. Zakładając, że utworzyłeś wcześniej folder nadrzędny o nazwie C:\IMOLTP\, użyj następującego kodu, aby zastosować te dwa kroki:
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;
Następnym krokiem jest utworzenie typu tabeli zoptymalizowanej pod kątem pamięci jako szablonu dla naszej zmiennej tabeli, uruchamiając następujący kod:
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);
Wtedy zamiast oryginalnej deklaracji zmiennej tabeli @Result użyjesz następującego kodu:
DECLARE @Result AS dbo.TYPE_closestmatch;
Oto kompletny kod rozwiązania:
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;
Przy wdrożonym optymalnym indeksowaniu rozwiązanie to zajęło 27 sekund na moim komputerze (w porównaniu do 38 sekund w przypadku zmiennej tabeli opartej na dysku), a bez optymalnego indeksowania ukończenie zajęło 29 sekund (w porównaniu do 40 sekund). To prawie 30-procentowe skrócenie czasu pracy.
Rozwiązanie 5, użycie SQL CLR
Innym sposobem na dalszą poprawę wydajności podejścia iteracyjnego jest wdrożenie rozwiązania przy użyciu SQL CLR, biorąc pod uwagę, że większość kosztów rozwiązania T-SQL wynika z nieefektywności pobierania kursora i zapętlania w T-SQL.
Oto kompletny kod rozwiązania implementujący ten sam algorytm, którego użyłem w rozwiązaniach 3 i 4 z C#, używając obiektów SqlDataReader zamiast kursorów T-SQL:
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(); } } }
Aby połączyć się z bazą danych, normalnie użyjesz opcji „context connection=true” zamiast pełnego ciągu połączenia. Niestety ta opcja nie jest dostępna, gdy musisz pracować z wieloma aktywnymi zestawami wyników. Nasze rozwiązanie emulujące pracę równoległą z dwoma kursorami przy użyciu dwóch obiektów SqlDataReader, dlatego potrzebujesz pełnego ciągu połączenia z opcją MultipleActiveResultSets=true. Oto pełne parametry połączenia:
"data source=MyServer\\MyInstance;Database=testdb;Trusted_Connection=True;MultipleActiveResultSets=true;"
Oczywiście w twoim przypadku musisz zastąpić MyServer\\MyInstance nazwami swojego serwera i instancji (jeśli dotyczy).
Ponadto fakt, że nie użyto „context connection=true” zamiast jawnego ciągu połączenia oznacza, że zestaw potrzebuje dostępu do zasobu zewnętrznego i dlatego jest zaufany. Zwykle można to osiągnąć, podpisując go certyfikatem lub kluczem asymetrycznym, który ma odpowiedni login z odpowiednimi uprawnieniami, lub umieszczając go na białej liście za pomocą procedury sp_add_trusted_assembly. Dla uproszczenia ustawię opcję bazy danych ZAUFANE na ON i określę zestaw uprawnień EXTERNAL_ACCESS podczas tworzenia zestawu. Poniższy kod wdraża rozwiązanie w bazie danych:
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;
Kod włącza CLR w instancji, wyłącza opcję bezpieczeństwa CLR strict, ustawia opcję bazy danych ZAUFANE na ON, tworzy zestaw i tworzy procedurę GetClosestMatches.
Użyj następującego kodu, aby przetestować procedurę składowaną:
EXEC dbo.GetClosestMatches;
Rozwiązanie CLR zajęło 8 sekund w moim systemie z optymalnym indeksowaniem i 9 sekund bez. To całkiem imponująca poprawa wydajności w porównaniu do wszystkich innych rozwiązań — zarówno relacyjnych, jak i iteracyjnych.
Wniosek
Rozwiązania iteracyjne są zazwyczaj niemile widziane w społeczności SQL, ponieważ nie są zgodne z modelem relacyjnym. Rzeczywistość jest jednak taka, że czasami nie jesteś w stanie stworzyć dobrze działających rozwiązań relacyjnych, a wydajność jest priorytetem. Stosując podejście iteracyjne, nie ograniczasz się do algorytmów, do których optymalizator programu SQL Server ma dostęp, ale raczej możesz zaimplementować dowolny algorytm. Jak zademonstrowano w tym artykule, używając algorytmu podobnego do łączenia, można było wykonać zadanie za pomocą jednego uporządkowanego przejścia dla każdego z danych wejściowych. Używając kursorów T-SQL i zmiennej tabeli opartej na dysku, uzyskałeś rozsądną wydajność i skalowanie. Udało Ci się poprawić wydajność o około 30 procent, przełączając się na zmienną tabelową zoptymalizowaną pod kątem pamięci, i znacznie więcej, używając SQL CLR.