sql >> Database teknologi >  >> RDS >> Database

Sammenligning af strengopdelings-/sammenkædningsmetoder

Tidligere på måneden offentliggjorde jeg et tip om noget, vi sikkert alle ville ønske, vi ikke behøvede at gøre:sortere eller fjerne dubletter fra afgrænsede strenge, der typisk involverer brugerdefinerede funktioner (UDF'er). Nogle gange har du brug for at samle listen igen (uden dubletterne) i alfabetisk rækkefølge, og nogle gange skal du muligvis bevare den oprindelige rækkefølge (det kan for eksempel være listen over nøglekolonner i et dårligt indeks).

Til min løsning, som adresserer begge scenarier, brugte jeg en taltabel sammen med et par brugerdefinerede funktioner (UDF'er) - den ene til at opdele strengen, den anden til at samle den igen. Du kan se det tip her:

  • Fjernelse af dubletter fra strenge i SQL Server

Selvfølgelig er der flere måder at løse dette problem på; Jeg gav kun én metode til at prøve, hvis du sidder fast med de strukturdata. Red-Gates @Phil_Factor fulgte op med et hurtigt indlæg, der viste hans tilgang, som undgår funktionerne og taltabellen og vælger i stedet for inline XML-manipulation. Han siger, at han foretrækker at have enkeltudsagnsforespørgsler og undgå både funktioner og række-for-række-behandling:

  • Afduplikering af adskilte lister i SQL Server

Så postede en læser, Steve Mangiameli, en looping-løsning som en kommentar til tippet. Hans begrundelse var, at brugen af ​​en taltabel forekom ham overdrevet.

Vi tre af os undlod alle at tage fat på et aspekt af dette, som normalt vil være ret vigtigt, hvis du udfører opgaven ofte nok eller på et hvilket som helst skalaniveau:ydeevne .

Test

Nysgerrig efter at se, hvor godt inline-XML og looping-tilgange ville fungere sammenlignet med min taltabelbaserede løsning, konstruerede jeg en fiktiv tabel til at udføre nogle tests; mit mål var 5.000 rækker med en gennemsnitlig strenglængde på mere end 250 tegn og mindst 10 elementer i hver streng. Med en meget kort cyklus af eksperimenter var jeg i stand til at opnå noget meget tæt på dette med følgende kode:

CREATE TABLE dbo.SourceTable
(
  [RowID]         int IDENTITY(1,1) PRIMARY KEY CLUSTERED,
  DelimitedString varchar(8000)
);
GO
 
;WITH s(s) AS 
(
 SELECT TOP (250) o.name + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
  (
   SELECT N'/column_' + c.name 
    FROM sys.all_columns AS c
    WHERE c.[object_id] = o.[object_id]
    ORDER BY NEWID()
    FOR XML PATH(N''), TYPE).value(N'.[1]', N'nvarchar(max)'
   ),
   -- make fake duplicates using 5 most common column names:
   N'/column_name/',        N'/name/name/foo/name/name/id/name/'),
   N'/column_status/',      N'/id/status/blat/status/foo/status/name/'),
   N'/column_type/',        N'/type/id/name/type/id/name/status/id/type/'),
   N'/column_object_id/',   N'/object_id/blat/object_id/status/type/name/'),
   N'/column_pdw_node_id/', N'/pdw_node_id/name/pdw_node_id/name/type/name/')
 FROM sys.all_objects AS o
 WHERE EXISTS 
 (
  SELECT 1 FROM sys.all_columns AS c 
  WHERE c.[object_id] = o.[object_id]
 )
 ORDER BY NEWID()
)
INSERT dbo.SourceTable(DelimitedString)
SELECT s FROM s;
GO 20

Dette producerede en tabel med eksempelrækker, der ser sådan ud (værdier afkortet):

RowID    DelimitedString
-----    ---------------
1        master_files/column_redo_target_fork_guid/.../column_differential_base_lsn/...
2        allocation_units/column_used_pages/.../column_data_space_id/type/id/name/type/...
3        foreign_key_columns/column_parent_object_id/column_constraint_object_id/...

Dataene som helhed havde følgende profil, som burde være god nok til at afdække eventuelle potentielle problemer med ydeevnen:

;WITH cte([Length], ElementCount) AS 
(
  SELECT 1.0*LEN(DelimitedString),
    1.0*LEN(REPLACE(DelimitedString,'/',''))
  FROM dbo.SourceTable
)
SELECT row_count = COUNT(*),
 avg_size     = AVG([Length]),
 max_size     = MAX([Length]),
 avg_elements = AVG(1 + [Length]-[ElementCount]),
 sum_elements = SUM(1 + [Length]-[ElementCount])
FROM cte;
 
EXEC sys.sp_spaceused N'dbo.SourceTable';
 
/* results (numbers may vary slightly, depending on SQL Server version the user objects in your database):
 
row_count    avg_size      max_size    avg_elements    sum_elements
---------    ----------    --------    ------------    ------------
5000         299.559000    2905.0      17.650000       88250.0
 
 
reserved    data       index_size    unused
--------    -------    ----------    ------
1672 KB     1648 KB    16 KB         8 KB
*/

Bemærk, at jeg skiftede til varchar her fra nvarchar i den originale artikel, fordi prøverne Phil og Steve leverede antog varchar , strenge med maksimalt 255 eller 8000 tegn, enkelttegns-afgrænsninger osv. Jeg har lært min lektie på den hårde måde, at hvis du vil tage en persons funktion og inkludere den i præstationssammenligninger, ændrer du så lidt som muligt – ideelt set ingenting. I virkeligheden ville jeg altid bruge nvarchar og ikke antage noget om den længste streng muligt. I dette tilfælde vidste jeg, at jeg ikke mistede nogen data, fordi den længste streng kun er på 2.905 tegn, og i denne database har jeg ingen tabeller eller kolonner, der bruger Unicode-tegn.

Dernæst oprettede jeg mine funktioner (som kræver en taltabel). En læser fik øje på et problem i funktionen i mit tip, hvor jeg gik ud fra, at afgrænsningen altid ville være et enkelt tegn, og rettede det her. Jeg konverterede også næsten alt til varchar(8000) at udjævne vilkårene med hensyn til strengtyper og længder.

DECLARE @UpperLimit INT = 1000000;
 
;WITH n(rn) AS
(
  SELECT ROW_NUMBER() OVER (ORDER BY s1.[object_id])
  FROM sys.all_columns AS s1
  CROSS JOIN sys.all_columns AS s2
)
SELECT [Number] = rn
INTO dbo.Numbers FROM n
WHERE rn <= @UpperLimit;
 
CREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers([Number]);
GO
 
CREATE FUNCTION [dbo].[SplitString] -- inline TVF
(
  @List  varchar(8000),
  @Delim varchar(32)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
  RETURN
  (
    SELECT 
      rn, 
      vn = ROW_NUMBER() OVER (PARTITION BY [Value] ORDER BY rn), 
      [Value]
    FROM 
    ( 
      SELECT 
        rn = ROW_NUMBER() OVER (ORDER BY CHARINDEX(@Delim, @List + @Delim)),
        [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
                  CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
      FROM dbo.Numbers
      WHERE Number <= LEN(@List)
      AND SUBSTRING(@Delim + @List, [Number], LEN(@Delim)) = @Delim
    ) AS x
  );
GO
 
CREATE FUNCTION [dbo].[ReassembleString] -- scalar UDF
(
  @List  varchar(8000),
  @Delim varchar(32),
  @Sort  varchar(32)
)
RETURNS varchar(8000)
WITH SCHEMABINDING
AS
BEGIN
  RETURN 
  ( 
    SELECT newval = STUFF((
     SELECT @Delim + x.[Value] 
     FROM dbo.SplitString(@List, @Delim) AS x
     WHERE (x.vn = 1) -- filter out duplicates
     ORDER BY CASE @Sort
       WHEN 'OriginalOrder' THEN CONVERT(int, x.rn)
       WHEN 'Alphabetical'  THEN CONVERT(varchar(8000), x.[Value])
       ELSE CONVERT(SQL_VARIANT, NULL) END
     FOR XML PATH(''), TYPE).value(N'(./text())[1]',N'varchar(8000)'),1,LEN(@Delim),'')
  );
END
GO

Dernæst oprettede jeg en enkelt, inline tabel-værdisat funktion, der kombinerede de to funktioner ovenfor, noget jeg nu ville ønske, jeg havde gjort i den originale artikel, for helt at undgå den skalære funktion. (Selvom det er sandt, at ikke alle skalarfunktioner er forfærdelige i skala, der er meget få undtagelser.)

CREATE FUNCTION [dbo].[RebuildString]
(
  @List  varchar(8000),
  @Delim varchar(32),
  @Sort  varchar(32)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
  RETURN
  ( 
    SELECT [Output] = STUFF((
     SELECT @Delim + x.[Value] 
     FROM 
	 ( 
	   SELECT rn, [Value], vn = ROW_NUMBER() OVER (PARTITION BY [Value] ORDER BY rn)
	   FROM      
	   ( 
	     SELECT rn = ROW_NUMBER() OVER (ORDER BY CHARINDEX(@Delim, @List + @Delim)),
           [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
                  CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
         FROM dbo.Numbers
         WHERE Number <= LEN(@List)
         AND SUBSTRING(@Delim + @List, [Number], LEN(@Delim)) = @Delim
	   ) AS y 
     ) AS x
     WHERE (x.vn = 1)
     ORDER BY CASE @Sort
       WHEN 'OriginalOrder' THEN CONVERT(int, x.rn)
       WHEN 'Alphabetical'  THEN CONVERT(varchar(8000), x.[Value])
       ELSE CONVERT(sql_variant, NULL) END
     FOR XML PATH(''), TYPE).value(N'(./text())[1]',N'varchar(8000)'),1,LEN(@Delim),'')
  );
GO

Jeg oprettede også separate versioner af den inline TVF, der var dedikeret til hver af de to sorteringsvalg, for at undgå ustabiliteten i CASE udtryk, men det viste sig slet ikke at have nogen dramatisk indflydelse.

Så lavede jeg Steves to funktioner:

CREATE FUNCTION [dbo].[gfn_ParseList] -- multi-statement TVF
  (@strToPars VARCHAR(8000), @parseChar CHAR(1))
RETURNS @parsedIDs TABLE
   (ParsedValue VARCHAR(255), PositionID INT IDENTITY)
AS
BEGIN
DECLARE 
  @startPos INT = 0
  , @strLen INT = 0
 
WHILE LEN(@strToPars) >= @startPos
  BEGIN
    IF (SELECT CHARINDEX(@parseChar,@strToPars,(@startPos+1))) > @startPos
      SELECT @strLen  = CHARINDEX(@parseChar,@strToPars,(@startPos+1))  - @startPos
    ELSE
      BEGIN
        SET @strLen = LEN(@strToPars) - (@startPos -1)
 
        INSERT @parsedIDs
        SELECT RTRIM(LTRIM(SUBSTRING(@strToPars,@startPos, @strLen)))
 
        BREAK
      END
 
    SELECT @strLen  = CHARINDEX(@parseChar,@strToPars,(@startPos+1))  - @startPos
 
    INSERT @parsedIDs
    SELECT RTRIM(LTRIM(SUBSTRING(@strToPars,@startPos, @strLen)))
    SET @startPos = @startPos+@strLen+1
  END
RETURN
END  
GO
 
CREATE FUNCTION [dbo].[ufn_DedupeString] -- scalar UDF
(
  @dupeStr VARCHAR(MAX), @strDelimiter CHAR(1), @maintainOrder BIT
)
-- can't possibly return nvarchar, but I'm not touching it
RETURNS NVARCHAR(MAX)
AS
BEGIN  
  DECLARE @tblStr2Tbl  TABLE (ParsedValue VARCHAR(255), PositionID INT);
  DECLARE @tblDeDupeMe TABLE (ParsedValue VARCHAR(255), PositionID INT);
 
  INSERT @tblStr2Tbl
  SELECT DISTINCT ParsedValue, PositionID FROM dbo.gfn_ParseList(@dupeStr,@strDelimiter);  
 
  WITH cteUniqueValues
  AS
  (
    SELECT DISTINCT ParsedValue
    FROM @tblStr2Tbl
  )
  INSERT @tblDeDupeMe
  SELECT d.ParsedValue
    , CASE @maintainOrder
        WHEN 1 THEN MIN(d.PositionID)
      ELSE ROW_NUMBER() OVER (ORDER BY d.ParsedValue)
    END AS PositionID
  FROM cteUniqueValues u
    JOIN @tblStr2Tbl d ON d.ParsedValue=u.ParsedValue
  GROUP BY d.ParsedValue
  ORDER BY d.ParsedValue
 
  DECLARE 
    @valCount INT
  , @curValue VARCHAR(255) =''
  , @posValue INT=0
  , @dedupedStr VARCHAR(4000)=''; 
 
  SELECT @valCount = COUNT(1) FROM @tblDeDupeMe;
  WHILE @valCount > 0
  BEGIN
    SELECT @posValue=a.minPos, @curValue=d.ParsedValue
    FROM (SELECT MIN(PositionID) minPos FROM @tblDeDupeMe WHERE PositionID  > @posValue) a
      JOIN @tblDeDupeMe d ON d.PositionID=a.minPos;
 
    SET @dedupedStr+=@curValue;
    SET @valCount-=1;
 
    IF @valCount > 0
      SET @dedupedStr+='/';
  END
  RETURN @dedupedStr;
END
GO

Derefter sætter jeg Phils direkte forespørgsler ind i min testrig (bemærk, at hans forespørgsler koder &lt; som &lt; for at beskytte dem mod XML-parsingsfejl, men de koder ikke > eller & – Jeg har tilføjet pladsholdere, hvis du skal beskytte dig mod strenge, der potentielt kan indeholde disse problematiske tegn):

-- Phil's query for maintaining original order
 
SELECT /*the re-assembled list*/
  stuff(
    (SELECT  '/'+TheValue  FROM
            (SELECT  x.y.value('.','varchar(20)') AS Thevalue,
                row_number() OVER (ORDER BY (SELECT 1)) AS TheOrder
                FROM XMLList.nodes('/list/i/text()') AS x ( y )
         )Nodes(Thevalue,TheOrder)
       GROUP BY TheValue
         ORDER BY min(TheOrder)
         FOR XML PATH('')
        ),1,1,'')
   as Deduplicated
FROM (/*XML version of the original list*/
  SELECT convert(XML,'<list><i>'
         --+replace(replace(
         +replace(replace(ASCIIList,'<','&lt;') --,'>','&gt;'),'&','&amp;')
	 ,'/','</i><i>')+'</i></list>')
   FROM (SELECT DelimitedString FROM dbo.SourceTable
   )XMLlist(AsciiList)
 )lists(XMLlist);
 
 
-- Phil's query for alpha
 
SELECT 
  stuff( (SELECT  DISTINCT '/'+x.y.value('.','varchar(20)')
                  FROM XMLList.nodes('/list/i/text()') AS x ( y )
                  FOR XML PATH('')),1,1,'') as Deduplicated
  FROM (
  SELECT convert(XML,'<list><i>'
         --+replace(replace(
         +replace(replace(ASCIIList,'<','&lt;') --,'>','&gt;'),'&','&amp;')
	 ,'/','</i><i>')+'</i></list>')
   FROM (SELECT AsciiList FROM 
	 (SELECT DelimitedString FROM dbo.SourceTable)ListsWithDuplicates(AsciiList)
   )XMLlist(AsciiList)
 )lists(XMLlist);

Testriggen var dybest set disse to forespørgsler, og også følgende funktionskald. Da jeg havde valideret, at de alle returnerede de samme data, blandede jeg scriptet med DATEDIFF output og loggede det til en tabel:

-- Maintain original order
 
  -- My UDF/TVF pair from the original article
  SELECT UDF_Original = dbo.ReassembleString(DelimitedString, '/', 'OriginalOrder') 
  FROM dbo.SourceTable ORDER BY RowID;
 
  -- My inline TVF based on the original article
  SELECT TVF_Original = f.[Output] FROM dbo.SourceTable AS t
    CROSS APPLY dbo.RebuildString(t.DelimitedString, '/', 'OriginalOrder') AS f
    ORDER BY t.RowID;
 
  -- Steve's UDF/TVF pair:
  SELECT Steve_Original = dbo.ufn_DedupeString(DelimitedString, '/', 1) 
  FROM dbo.SourceTable;
 
  -- Phil's first query from above
 
-- Reassemble in alphabetical order
 
  -- My UDF/TVF pair from the original article
  SELECT UDF_Alpha = dbo.ReassembleString(DelimitedString, '/', 'Alphabetical') 
  FROM dbo.SourceTable ORDER BY RowID;
 
  -- My inline TVF based on the original article
  SELECT TVF_Alpha = f.[Output] FROM dbo.SourceTable AS t
    CROSS APPLY dbo.RebuildString(t.DelimitedString, '/', 'Alphabetical') AS f
    ORDER BY t.RowID;
 
  -- Steve's UDF/TVF pair:
  SELECT Steve_Alpha = dbo.ufn_DedupeString(DelimitedString, '/', 0) 
  FROM dbo.SourceTable;
 
  -- Phil's second query from above

Og så kørte jeg ydeevnetest på to forskellige systemer (en quad core med 8GB og en 8-core VM med 32GB) og i hvert tilfælde på både SQL Server 2012 og SQL Server 2016 CTP 3.2 (13.0.900.73).

Resultater

De resultater, jeg observerede, er opsummeret i det følgende diagram, som viser varigheden i millisekunder af hver type forespørgsel, gennemsnittet over alfabetisk og original rækkefølge, de fire server/version-kombinationer og en serie på 15 udførelser for hver permutation. Klik for at forstørre:

Dette viser, at taltabellen, selvom den blev anset for overkonstrueret, faktisk gav den mest effektive løsning (i hvert fald med hensyn til varighed). Dette var selvfølgelig bedre med den enkelte TVF, som jeg implementerede for nylig, end med de indlejrede funktioner fra den originale artikel, men begge løsninger kører rundt om de to alternativer.

For at komme mere i detaljer, her er nedbrydningerne for hver maskine, version og forespørgselstype, for at opretholde den oprindelige rækkefølge:

…og for at samle listen igen i alfabetisk rækkefølge:

Disse viser, at sorteringsvalget havde ringe indflydelse på resultatet - begge diagrammer er stort set identiske. Og det giver mening, for givet inputdataens form er der ikke noget indeks, jeg kan forestille mig, der ville gøre sorteringen mere effektiv – det er en iterativ tilgang, uanset hvordan du opdeler det, eller hvordan du returnerer dataene. Men det er klart, at nogle iterative tilgange generelt kan være værre end andre, og det er ikke nødvendigvis brugen af ​​en UDF (eller en taltabel), der gør dem på den måde.

Konklusion

Indtil vi har indbygget split- og sammenkædningsfunktionalitet i SQL Server, kommer vi til at bruge alle slags unintuitive metoder til at få arbejdet gjort, inklusive brugerdefinerede funktioner. Hvis du håndterer en enkelt streng ad gangen, vil du ikke se den store forskel. Men efterhånden som dine data skaleres op, vil det være umagen værd at teste forskellige tilgange (og jeg antyder på ingen måde, at metoderne ovenfor er de bedste, du finder – jeg kiggede f.eks. ikke engang på CLR, eller andre T-SQL-tilgange fra denne serie).


  1. MySQL &MariaDB Load Balancing med ProxySQL

  2. Hvorfor får jeg en java.lang.IllegalArgumentException:bindeværdien ved indeks 1 er null i dette tilfælde?

  3. Oprettelse af visuel database med MySQL Workbench

  4. Sådan indstilles tegnsættet og samlingen af ​​en database i MySQL