Database
 sql >> Baza danych >  >> RDS >> Database

Podziel struny we właściwy sposób – lub następny najlepszy sposób

AKTUALIZACJA:2 września 2021 (Pierwotnie opublikowany 26 lipca 2012 r.)

W ciągu kilku głównych wersji naszej ulubionej platformy bazodanowej wiele się zmieniło. SQL Server 2016 przyniósł nam STRING_SPLIT, natywną funkcję, która eliminuje potrzebę wielu niestandardowych rozwiązań, których potrzebowaliśmy wcześniej. Jest też szybki, ale nie idealny. Na przykład obsługuje tylko ogranicznik jednoznakowy i nie zwraca niczego, aby wskazać kolejność elementów wejściowych. Napisałem kilka artykułów na temat tej funkcji (i STRING_AGG, który pojawił się w SQL Server 2017) od czasu napisania tego posta:

  • Niespodzianki i założenia dotyczące wydajności:STRING_SPLIT()
  • STRING_SPLIT() w SQL Server 2016:kontynuacja nr 1
  • STRING_SPLIT() w SQL Server 2016:kontynuacja nr 2
  • Kod zastępujący dzielony ciąg SQL Server za pomocą STRING_SPLIT
  • Porównywanie metod dzielenia/konkatenacji ciągów
  • Rozwiąż stare problemy za pomocą nowych funkcji STRING_AGG i STRING_SPLIT w SQL Server
  • Zajmowanie się jednoznakowym ogranicznikiem w funkcji STRING_SPLIT serwera SQL
  • Pomóż przy ulepszeniach STRING_SPLIT
  • Sposób na ulepszenie STRING_SPLIT w SQL Server – a Ty możesz pomóc

Zostawię tutaj poniższą treść dla potomności i znaczenia historycznego, a także dlatego, że niektóre metodologie testowania są istotne dla innych problemów poza dzieleniem ciągów, ale proszę zapoznać się z niektórymi z powyższych odnośników, aby uzyskać informacje o tym, jak należy dzielić ciągi w nowoczesnych, obsługiwanych wersjach SQL Server – a także ten post, który wyjaśnia, dlaczego dzielenie ciągów może nie jest problemem, który chcesz rozwiązać w bazie danych, czy to nowa funkcja, czy nie.

  • Rozdzielanie ciągów:teraz z mniejszą ilością T-SQL

Wiem, że wiele osób jest znudzonych problemem „split strings”, ale wciąż wydaje się, że pojawia się on prawie codziennie na forum i stronach z pytaniami i odpowiedziami, takich jak Stack Overflow. Jest to problem polegający na tym, że ludzie chcą przekazać ciąg znaków taki jak ten:

EXEC dbo.UpdateProfile @UserID = 1, @FavoriteTeams = N'Patriots,Red Sox,Bruins';

W ramach procedury chcą zrobić coś takiego:

INSERT dbo.UserTeams(UserID, TeamID) SELECT @UserID, TeamID
    FROM dbo.Teams WHERE TeamName IN (@FavoriteTeams);

To nie działa, ponieważ @FavoriteTeams jest pojedynczym ciągiem, a powyższe przekłada się na:

INSERT dbo.UserTeams(UserID, TeamID) SELECT @UserID, TeamID 
    FROM dbo.Teams WHERE TeamName IN (N'Patriots,Red Sox,Bruins');

Dlatego SQL Server spróbuje znaleźć zespół o nazwie Patriots,Red Sox,Bruins i domyślam się, że takiego zespołu nie ma. To, czego naprawdę chcą tutaj, to odpowiednik:

INSERT dbo.UserTeams(UserID, TeamID) SELECT @UserID, TeamID
    FROM dbo.Teams WHERE TeamName IN (N'Patriots', N'Red Sox', N'Bruins');

Ale ponieważ w SQL Server nie ma typu tablicy, w ogóle nie tak interpretuje się zmienną – wciąż jest to prosty, pojedynczy ciąg, który zawiera przecinki. Pomijając wątpliwy projekt schematu, w tym przypadku listę oddzieloną przecinkami należy „podzielić” na poszczególne wartości – i to jest pytanie, które często wywołuje wiele „nowych” debat i komentarzy na temat najlepszego rozwiązania, aby to osiągnąć.

Prawie zawsze odpowiedź wydaje się być taka, że ​​powinieneś używać CLR. Jeśli nie możesz używać CLR – a wiem, że jest wielu z was, którzy nie mogą tego zrobić ze względu na politykę firmy, szefa ze spiczastymi włosami lub upór – wtedy używacie jednego z wielu istniejących obejść. Istnieje wiele obejść.

Ale którego należy użyć?

Porównam wydajność kilku rozwiązań – i skoncentruję się na pytaniu, które wszyscy zawsze zadają:„Który jest najszybszy?” Nie będę rozwodził się nad dyskusją wokół *wszystkich* potencjalnych metod, ponieważ kilka zostało już wyeliminowanych z powodu tego, że po prostu nie skalują się. Być może odwiedzę to ponownie w przyszłości, aby zbadać wpływ na inne wskaźniki, ale na razie skupię się tylko na czasie trwania. Oto konkurenci, których porównam (używając SQL Server 2012, 11.00.2316, na maszynie wirtualnej Windows 7 z 4 procesorami i 8 GB pamięci RAM):

CLR

Jeśli chcesz używać CLR, zdecydowanie powinieneś pożyczyć kod od innego MVP Adama Machanica, zanim pomyślisz o napisaniu własnego (wcześniej pisałem na blogu o ponownym wymyśleniu koła i dotyczy to również darmowych fragmentów kodu, takich jak ten). Spędził dużo czasu na dostrajaniu tej funkcji CLR, aby wydajnie analizować ciąg. Jeśli obecnie używasz funkcji CLR, a to nie jest to, zdecydowanie polecam ją wdrożyć i porównać – przetestowałem ją ze znacznie prostszą, opartą na VB procedurą CLR, która była funkcjonalnie równoważna, ale podejście VB działało około trzy razy gorzej niż Adama.

Wziąłem więc funkcję Adama, skompilowałem kod do biblioteki DLL (używając csc) i wdrożyłem właśnie ten plik na serwerze. Następnie dodałem następujący zestaw i funkcję do mojej bazy danych:

CREATE ASSEMBLY CLRUtilities FROM 'c:\DLLs\CLRUtilities.dll' 
  WITH PERMISSION_SET = SAFE;
GO
 
CREATE FUNCTION dbo.SplitStrings_CLR
(
   @List      NVARCHAR(MAX),
   @Delimiter NVARCHAR(255)
)
RETURNS TABLE ( Item NVARCHAR(4000) )
EXTERNAL NAME CLRUtilities.UserDefinedFunctions.SplitString_Multi;
GO
XML

Jest to typowa funkcja, której używam w jednorazowych scenariuszach, w których wiem, że dane wejściowe są „bezpieczne”, ale nie jest to funkcja, którą polecam w środowiskach produkcyjnych (więcej na ten temat poniżej).

CREATE FUNCTION dbo.SplitStrings_XML
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN 
   (  
      SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
      FROM 
      ( 
        SELECT x = CONVERT(XML, '<i>' 
          + REPLACE(@List, @Delimiter, '</i><i>') 
          + '</i>').query('.')
      ) AS a CROSS APPLY x.nodes('i') AS y(i)
   );
GO

W podejściu XML musi obowiązywać bardzo mocne zastrzeżenie:można go użyć tylko wtedy, gdy możesz zagwarantować, że twój ciąg wejściowy nie zawiera żadnych niedozwolonych znaków XML. Jedna nazwa z <,> lub &i funkcja zostanie wysadzony. Więc niezależnie od wydajności, jeśli zamierzasz użyć tego podejścia, pamiętaj o ograniczeniach – nie powinno to być uważane za realną opcję dla ogólnego rozdzielacza ciągów. Uwzględniam to w tym podsumowaniu, ponieważ możesz mieć przypadek, w którym możesz ufaj danemu wejściowemu – na przykład możliwe jest użycie dla oddzielonych przecinkami list liczb całkowitych lub identyfikatorów GUID.

Tabela liczb

To rozwiązanie wykorzystuje tabelę liczb, którą musisz sam zbudować i wypełnić. (Od wieków prosiliśmy o wersję wbudowaną). Tabela Numbers powinna zawierać wystarczającą liczbę wierszy, aby przekroczyć długość najdłuższego ciągu, który będzie dzielony. W tym przypadku użyjemy 1 000 000 wierszy:

SET NOCOUNT ON;
 
DECLARE @UpperLimit INT = 1000000;
 
WITH n AS
(
    SELECT
        x = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
    FROM       sys.all_objects AS s1
    CROSS JOIN sys.all_objects AS s2
    CROSS JOIN sys.all_objects AS s3
)
SELECT Number = x
  INTO dbo.Numbers
  FROM n
  WHERE x BETWEEN 1 AND @UpperLimit;
 
GO
CREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number) 
    WITH (DATA_COMPRESSION = PAGE);
GO

(Korzystanie z kompresji danych drastycznie zmniejszy liczbę wymaganych stron, ale oczywiście powinieneś używać tej opcji tylko wtedy, gdy korzystasz z wersji Enterprise Edition. W tym przypadku skompresowane dane wymagają 1360 stron, w porównaniu do 2102 stron bez kompresji – około 35% oszczędności. )

CREATE FUNCTION dbo.SplitStrings_Numbers
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN
   (
       SELECT Item = SUBSTRING(@List, Number, 
         CHARINDEX(@Delimiter, @List + @Delimiter, Number) - Number)
       FROM dbo.Numbers
       WHERE Number <= CONVERT(INT, LEN(@List))
         AND SUBSTRING(@Delimiter + @List, Number, LEN(@Delimiter)) = @Delimiter
   );
GO

Wspólne wyrażenie tabeli

To rozwiązanie wykorzystuje rekurencyjne CTE do wyodrębnienia każdej części ciągu z „pozostałości” poprzedniej części. Jako rekurencyjne CTE ze zmiennymi lokalnymi, zauważysz, że musiała to być wieloinstrukcyjna funkcja z wartościami tabelarycznymi, w przeciwieństwie do innych, które wszystkie są wbudowane.

CREATE FUNCTION dbo.SplitStrings_CTE
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS @Items TABLE (Item NVARCHAR(4000))
WITH SCHEMABINDING
AS
BEGIN
   DECLARE @ll INT = LEN(@List) + 1, @ld INT = LEN(@Delimiter);
 
   WITH a AS
   (
       SELECT
           [start] = 1,
           [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, 1), 0), @ll),
           [value] = SUBSTRING(@List, 1, 
                     COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, 1), 0), @ll) - 1)
       UNION ALL
       SELECT
           [start] = CONVERT(INT, [end]) + @ld,
           [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, [end] + @ld), 0), @ll),
           [value] = SUBSTRING(@List, [end] + @ld, 
                     COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, [end] + @ld), 0), @ll)-[end]-@ld)
       FROM a
       WHERE [end] < @ll ) INSERT @Items SELECT [value] FROM a WHERE LEN([value]) > 0
   OPTION (MAXRECURSION 0);
 
   RETURN;
END
GO

Rozgałęźnik Jeffa Modena Funkcja oparta na rozdzielaczu Jeffa Modena z niewielkimi zmianami w celu obsługi dłuższych ciągów

Powyżej na SQLServerCentral Jeff Moden zaprezentował funkcję rozdzielającą, która rywalizowała z wydajnością CLR, więc pomyślałem, że sprawiedliwe będzie uwzględnienie w tym podsumowaniu odmiany przy użyciu podobnego podejścia. Musiałem dokonać kilku drobnych zmian w jego funkcji, aby obsłużyć nasz najdłuższy ciąg (500 000 znaków), a także upodobnić konwencje nazewnictwa:

CREATE FUNCTION dbo.SplitStrings_Moden
(
   @List NVARCHAR(MAX),
   @Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
  WITH E1(N)        AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
                         UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 
                         UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
       E2(N)        AS (SELECT 1 FROM E1 a, E1 b),
       E4(N)        AS (SELECT 1 FROM E2 a, E2 b),
       E42(N)       AS (SELECT 1 FROM E4 a, E2 b),
       cteTally(N)  AS (SELECT 0 UNION ALL SELECT TOP (DATALENGTH(ISNULL(@List,1))) 
                         ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E42),
       cteStart(N1) AS (SELECT t.N+1 FROM cteTally t
                         WHERE (SUBSTRING(@List,t.N,1) = @Delimiter OR t.N = 0))
  SELECT Item = SUBSTRING(@List, s.N1, ISNULL(NULLIF(CHARINDEX(@Delimiter,@List,s.N1),0)-s.N1,8000))
    FROM cteStart s;

Na marginesie, dla tych, którzy używają rozwiązania Jeffa Modena, możesz rozważyć użycie tabeli liczb jak powyżej i poeksperymentowanie z niewielką zmianą funkcji Jeffa:

CREATE FUNCTION dbo.SplitStrings_Moden2
(
   @List      NVARCHAR(MAX),
   @Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
   WITH cteTally(N)  AS 
   (
	   SELECT TOP (DATALENGTH(ISNULL(@List,1))+1) Number-1 
	      FROM dbo.Numbers ORDER BY Number
   ),
   cteStart(N1) AS 
   (
       SELECT t.N+1 
          FROM cteTally t
    	  WHERE (SUBSTRING(@List,t.N,1) = @Delimiter OR t.N = 0)
   )
   SELECT Item = SUBSTRING(@List, s.N1, 
      ISNULL(NULLIF(CHARINDEX(@Delimiter, @List, s.N1), 0) - s.N1, 8000))
   FROM cteStart AS s;

(To zamieni nieco wyższe odczyty na nieco niższy procesor, więc może być lepsze w zależności od tego, czy twój system jest już powiązany z procesorem, czy we/wy).

Sprawdzanie stanu zdrowia

Aby mieć pewność, że jesteśmy na dobrej drodze, możemy sprawdzić, czy wszystkie pięć funkcji zwraca oczekiwane wyniki:

DECLARE @s NVARCHAR(MAX) = N'Patriots,Red Sox,Bruins';
 
SELECT Item FROM dbo.SplitStrings_CLR     (@s, N',');
SELECT Item FROM dbo.SplitStrings_XML     (@s, N',');
SELECT Item FROM dbo.SplitStrings_Numbers (@s, N',');
SELECT Item FROM dbo.SplitStrings_CTE     (@s, N',');
SELECT Item FROM dbo.SplitStrings_Moden   (@s, N',');

W rzeczywistości są to wyniki, które widzimy we wszystkich pięciu przypadkach…

Dane testowe

Teraz, gdy wiemy, że funkcje zachowują się zgodnie z oczekiwaniami, możemy przejść do zabawnej części:testowania wydajności na różnych liczbach ciągów znaków o różnej długości. Ale najpierw potrzebujemy stołu. Stworzyłem następujący prosty obiekt:

CREATE TABLE dbo.strings
(
  string_type  TINYINT,
  string_value NVARCHAR(MAX)
);
 
CREATE CLUSTERED INDEX st ON dbo.strings(string_type);

Wypełniłem tę tabelę zestawem ciągów o różnej długości, upewniając się, że w każdym teście zostanie użyty mniej więcej ten sam zestaw danych – najpierw 10 000 wierszy, w których ciąg ma 50 znaków, a następnie 1000 wierszy, w których ciąg ma 500 znaków , 100 wierszy, w których ciąg ma 5 000 znaków, 10 wierszy, w których ciąg ma 50 000 znaków, i tak dalej do 1 wiersza zawierającego 500 000 znaków. Zrobiłem to zarówno po to, aby porównać tę samą ilość ogólnych danych przetwarzanych przez funkcje, jak również po to, aby moje czasy testowania były nieco przewidywalne.

Używam tabeli #temp, dzięki czemu mogę po prostu użyć GO do wykonania każdej partii określoną liczbę razy:

SET NOCOUNT ON;
GO
CREATE TABLE #x(s NVARCHAR(MAX));
INSERT #x SELECT N'a,id,xyz,abcd,abcde,sa,foo,bar,mort,splunge,bacon,';
GO
INSERT dbo.strings SELECT 1, s FROM #x;
GO 10000
INSERT dbo.strings SELECT 2, REPLICATE(s,10) FROM #x;
GO 1000
INSERT dbo.strings SELECT 3, REPLICATE(s,100) FROM #x;
GO 100
INSERT dbo.strings SELECT 4, REPLICATE(s,1000) FROM #x;
GO 10
INSERT dbo.strings SELECT 5, REPLICATE(s,10000) FROM #x;
GO
DROP TABLE #x;
GO
 
-- then to clean up the trailing comma, since some approaches treat a trailing empty string as a valid element:
UPDATE dbo.strings SET string_value = SUBSTRING(string_value, 1, LEN(string_value)-1) + 'x';

Utworzenie i wypełnienie tej tabeli na moim komputerze zajęło około 20 sekund, a tabela reprezentuje około 6 MB danych (około 500 000 znaków razy 2 bajty lub 1 MB na string_type plus narzut wierszy i indeksów). Nie jest to duża tabela, ale powinna być wystarczająco duża, aby podkreślić wszelkie różnice w wydajności między funkcjami.

Testy

Mając funkcje na swoim miejscu i tabelę odpowiednio wypełnioną dużymi ciągami do przeżuwania, możemy wreszcie przeprowadzić kilka rzeczywistych testów, aby zobaczyć, jak różne funkcje działają na rzeczywistych danych. Aby zmierzyć wydajność bez uwzględniania obciążenia sieci, użyłem SQL Sentry Plan Explorer, uruchamiając każdy zestaw testów 10 razy, zbierając metryki czasu trwania i uśredniając.

Pierwszy test po prostu wyciągnął elementy z każdego ciągu jako zestaw:

DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
 
DECLARE @string_type TINYINT = ; -- 1-5 from above
 
SELECT t.Item FROM dbo.strings AS s
  CROSS APPLY dbo.SplitStrings_(s.string_value, ',') AS t
  WHERE s.string_type = @string_type;

Wyniki pokazują, że gdy struny stają się większe, przewaga CLR naprawdę błyszczy. W dolnej części wyniki były mieszane, ale znowu metoda XML powinna mieć obok siebie gwiazdkę, ponieważ jej użycie zależy od danych wejściowych bezpiecznych dla XML. W tym konkretnym przypadku użycia tabela Liczb konsekwentnie wypadała najgorzej:


Czas trwania w milisekundach

Po hiperbolicznym 40-sekundowym występie tabeli liczb w 10 rzędach po 50 000 znaków, odrzuciłem go z biegu w ostatnim teście. Aby lepiej pokazać względną wydajność czterech najlepszych metod w tym teście, całkowicie pominąłem wyniki liczb z wykresu:

Następnie porównajmy, kiedy przeprowadzamy wyszukiwanie względem wartości oddzielonych przecinkami (np. zwracamy wiersze, w których jednym z ciągów jest „foo”). Ponownie użyjemy pięciu powyższych funkcji, ale porównamy również wynik z wyszukiwaniem przeprowadzonym w czasie wykonywania przy użyciu funkcji LIKE zamiast zawracania sobie głowy dzieleniem.

DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
 
DECLARE @i INT = , @search NVARCHAR(32) = N'foo';
 
;WITH s(st, sv) AS 
(
  SELECT string_type, string_value
    FROM dbo.strings AS s
    WHERE string_type = @i
)
SELECT s.string_type, s.string_value FROM s 
  CROSS APPLY dbo.SplitStrings_(s.sv, ',') AS t
  WHERE t.Item = @search;
 
SELECT s.string_type
  FROM dbo.strings
  WHERE string_type = @i
  AND ',' + string_value + ',' LIKE '%,' + @search + ',%';

Wyniki te pokazują, że w przypadku małych ciągów CLR był w rzeczywistości najwolniejszy, a najlepszym rozwiązaniem będzie wykonanie skanowania przy użyciu funkcji LIKE, bez zawracania sobie głowy dzieleniem danych. Ponownie porzuciłem rozwiązanie tabeli liczb z piątego podejścia, kiedy stało się jasne, że jego czas trwania będzie wzrastał wykładniczo wraz ze wzrostem rozmiaru łańcucha:


Czas trwania w milisekundach

Aby lepiej zademonstrować wzorce dla 4 najlepszych wyników, wyeliminowałem z wykresu rozwiązania Numbers i XML:

Następnie spójrzmy na replikację przypadku użycia z początku tego postu, w którym próbujemy znaleźć wszystkie wiersze w jednej tabeli, które istnieją na liście, która jest przekazywana. Podobnie jak w przypadku danych w tabeli, którą utworzyliśmy powyżej, utworzymy ciągi o różnej długości od 50 do 500 000 znaków, zapiszemy je w zmiennej, a następnie sprawdzimy, czy na liście znajduje się wspólny widok katalogu.

DECLARE 
  @i INT = , -- value 1-5, yielding strings 50 - 500,000 characters
  @x NVARCHAR(MAX) = N'a,id,xyz,abcd,abcde,sa,foo,bar,mort,splunge,bacon,';
 
SET @x = REPLICATE(@x, POWER(10, @i-1));
 
SET @x = SUBSTRING(@x, 1, LEN(@x)-1) + 'x';
 
SELECT c.[object_id] 
  FROM sys.all_columns AS c
  WHERE EXISTS 
  (
    SELECT 1 FROM dbo.SplitStrings_(@x, N',') AS x 
    WHERE Item = c.name
  )
  ORDER BY c.[object_id];
 
SELECT [object_id]
  FROM sys.all_columns 
  WHERE N',' + @x + ',' LIKE N'%,' + name + ',%'
  ORDER BY [object_id];

Wyniki te pokazują, że w przypadku tego wzorca czas trwania kilku metod rośnie wykładniczo wraz ze wzrostem rozmiaru struny. Na niższym poziomie XML utrzymuje dobre tempo z CLR, ale to również szybko się pogarsza. CLR jest tutaj niezmiennie wyraźnym zwycięzcą:


Czas trwania w milisekundach

I znowu bez metod, które eksplodują w górę pod względem czasu trwania:

Na koniec porównajmy koszt pobrania danych z pojedynczej zmiennej o różnej długości, pomijając koszt odczytu danych z tabeli. Ponownie wygenerujemy ciągi o różnej długości, od 50 do 500 000 znaków, a następnie po prostu zwrócimy wartości jako zestaw:

DECLARE 
  @i INT = , -- value 1-5, yielding strings 50 - 500,000 characters
  @x NVARCHAR(MAX) = N'a,id,xyz,abcd,abcde,sa,foo,bar,mort,splunge,bacon,';
 
SET @x = REPLICATE(@x, POWER(10, @i-1));
 
SET @x = SUBSTRING(@x, 1, LEN(@x)-1) + 'x';
 
SELECT Item FROM dbo.SplitStrings_(@x, N',');

Wyniki te pokazują również, że CLR jest dość płaski pod względem czasu trwania, aż do 110 000 pozycji w zestawie, podczas gdy inne metody utrzymują przyzwoite tempo do pewnego czasu po 11 000 pozycji:


Czas trwania w milisekundach

Wniosek

W prawie wszystkich przypadkach rozwiązanie CLR wyraźnie przewyższa inne podejścia – w niektórych przypadkach jest to zwycięstwo osuwiskowe, zwłaszcza w miarę wzrostu rozmiarów strun; w kilku innych jest to wykończenie zdjęcia, które może spaść w obie strony. W pierwszym teście zobaczyliśmy, że XML i CTE przewyższają CLR na najniższym poziomie, więc jeśli jest to typowy przypadek użycia *i* masz pewność, że twoje ciągi mają zakres od 1 do 10 000 znaków, jedno z tych podejść może być lepszą opcją. Jeśli rozmiary twoich strun są mniej przewidywalne niż to, CLR jest prawdopodobnie nadal najlepszym rozwiązaniem – tracisz kilka milisekund na dolnym końcu, ale dużo zyskujesz na górnym końcu. Oto wybory, których dokonałbym w zależności od zadania, z wyróżnieniem drugiego miejsca w przypadkach, w których CLR nie jest opcją. Zauważ, że XML jest moją preferowaną metodą tylko wtedy, gdy wiem, że dane wejściowe są bezpieczne dla XML; niekoniecznie są to najlepsze alternatywy, jeśli masz mniejszą wiarę w swój wkład.

Jedynym prawdziwym wyjątkiem, w którym CLR nie jest moim wyborem, jest przypadek, w którym faktycznie przechowujesz listy oddzielone przecinkami w tabeli, a następnie znajdujesz wiersze, w których zdefiniowana encja znajduje się na tej liście. W tym konkretnym przypadku prawdopodobnie najpierw zaleciłbym przeprojektowanie i prawidłową normalizację schematu, aby te wartości były przechowywane osobno, zamiast używać ich jako wymówki, aby nie używać CLR do dzielenia.

Jeśli nie możesz używać CLR z innych powodów, nie ma wyraźnego „drugiego miejsca” ujawnionego przez te testy; moje odpowiedzi powyżej opierały się na ogólnej skali, a nie na żadnym konkretnym rozmiarze ciągu. Każde rozwiązanie tutaj znalazło się na drugim miejscu w co najmniej jednym scenariuszu – więc chociaż CLR jest wyraźnie wyborem, kiedy możesz go użyć, to, czego powinieneś użyć, gdy nie możesz, jest bardziej odpowiedzią „to zależy” – musisz oceniać na podstawie Twoje przypadki użycia i powyższe testy (lub konstruując własne testy), która alternatywa jest dla Ciebie lepsza.

Uzupełnienie:alternatywa dla dzielenia w pierwszej kolejności

Powyższe podejścia nie wymagają żadnych zmian w istniejących aplikacjach, zakładając, że już gromadzą ciąg oddzielony przecinkami i wrzucają go do bazy danych, aby się z nim uporać. Jedną z opcji, którą należy rozważyć, jeśli CLR nie jest opcją i/lub można zmodyfikować aplikację (aplikacje), jest użycie parametrów z wartościami tabelarycznymi (TVP). Oto szybki przykład wykorzystania TVP w powyższym kontekście. Najpierw utwórz typ tabeli z kolumną z pojedynczym ciągiem:

CREATE TYPE dbo.Items AS TABLE
(
  Item NVARCHAR(4000)
);

Następnie procedura składowana może przyjąć to TVP jako dane wejściowe i dołączyć do treści (lub użyć jej w inny sposób – to tylko jeden przykład):

CREATE PROCEDURE dbo.UpdateProfile
    @UserID INT,
    @TeamNames dbo.Items READONLY
AS
BEGIN
   SET NOCOUNT ON;
 
   INSERT dbo.UserTeams(UserID, TeamID) SELECT @UserID, t.TeamID
      FROM dbo.Teams AS t
      INNER JOIN @TeamNames AS tn
      ON t.Name = tn.Item;
END
GO

Teraz w kodzie C#, na przykład, zamiast tworzyć ciąg rozdzielany przecinkami, wypełnij DataTable (lub użyj dowolnej zgodnej kolekcji, która może już zawierać Twój zestaw wartości):

DataTable tvp = new DataTable();
tvp.Columns.Add(new DataColumn("Item"));
 
// in a loop from a collection, presumably:
tvp.Rows.Add(someThing.someValue);
 
using (connectionObject)
{
    SqlCommand cmd       = new SqlCommand("dbo.UpdateProfile", connectionObject);
    cmd.CommandType      = CommandType.StoredProcedure;
    SqlParameter tvparam = cmd.Parameters.AddWithValue("@TeamNames", tvp);
    tvparam.SqlDbType    = SqlDbType.Structured;
    // other parameters, e.g. userId
    cmd.ExecuteNonQuery();
}

Możesz uznać to za prequel kolejnego posta.

Oczywiście nie działa to dobrze z JSON i innymi interfejsami API – dość często jest to powód, dla którego ciąg oddzielony przecinkami jest przekazywany do SQL Server.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Zliczanie odwołań do rekordu w tabeli za pomocą kluczy obcych

  2. Pythonowe interfejsy API REST z Flask, Connexion i SQLAlchemy — część 2

  3. Jak zainstalować Apache Cassandra na Ubuntu 20.10/Ubuntu 20.04

  4. Pomiar wydajności bazy danych pod presją

  5. Jak zainstalować Nextcloud 15 na Ubuntu 18.04