sql >> データベース >  >> RDS >> Database

文字列の分割/連結方法の比較

    今月初めに、私たち全員がやらなくてもよかったと思うことについてのヒントを公開しました。通常はユーザー定義関数(UDF)を含む、区切られた文字列から重複を並べ替えたり削除したりします。アルファベット順に(重複なしで)リストを再構成する必要がある場合もあれば、元の順序を維持する必要がある場合もあります(たとえば、不良インデックスのキー列のリストである可能性があります)。

    両方のシナリオに対応する私のソリューションでは、数値テーブルと、ユーザー定義関数(UDF)のペアを使用しました。1つは文字列を分割し、もう1つは文字列を再構築します。ここでそのヒントを見ることができます:

    • SQLServerの文字列からの重複の削除

    もちろん、この問題を解決する方法は複数あります。私は、あなたがその構造データで立ち往生している場合に試すための1つの方法を提供しているだけでした。 Red-Gateの@Phil_Factorは、彼のアプローチを示す簡単な投稿でフォローアップしました。これは、関数と数値テーブルを避け、代わりにインラインXML操作を選択します。彼は、単一ステートメントのクエリを使用し、関数と行ごとの処理の両方を回避することを好むと述べています。

    • SQLServerでの区切りリストの重複排除

    次に、読者のSteve Mangiameliが、ヒントのコメントとしてループソリューションを投稿しました。彼の推論は、数値テーブルの使用は彼にとって過剰に設計されているように思われたというものでした。

    私たち3人は全員、タスクを十分な頻度で、または任意のレベルの規模で実行している場合に通常非常に重要になるこの側面に対処できませんでした。パフォーマンス

    テスト

    インラインXMLとループのアプローチが、数値テーブルベースのソリューションと比較してどれだけうまく機能するかを知りたいと思ったので、いくつかのテストを実行するために架空のテーブルを作成しました。私の目標は5,000行で、平均文字列長は250文字を超え、各文字列には少なくとも10個の要素が含まれていました。非常に短い実験サイクルで、次のコードを使用してこれに非常に近いものを達成することができました。

    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

    これにより、次のようなサンプル行を含むテーブルが作成されました(値は切り捨てられます):

    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/...

    データ全体として、次のプロファイルがありました。これは、潜在的なパフォーマンスの問題を明らかにするのに十分なはずです。

    ;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
    */

    varcharに切り替えたことに注意してください ここからnvarchar 元の記事では、PhilとSteveが提供したサンプルはvarcharを想定しているためです。 、255文字または8000文字で制限される文字列、1文字の区切り文字など。私のレッスンでは、誰かの関数を取得してパフォーマンスの比較に含める場合、変更はわずかであるという難しい方法を学びました。可能–理想的には何もありません。実際には、私は常にnvarcharを使用します 可能な限り長い文字列については何も想定しないでください。この場合、最長の文字列は2,905文字しかないため、データが失われることはなく、このデータベースにはUnicode文字を使用するテーブルや列がありません。

    次に、関数を作成しました(数値テーブルが必要です)。ある読者が私のヒントの関数の問題を発見しました。区切り文字は常に1文字であると想定し、ここで修正しました。また、ほぼすべてをvarchar(8000)に変換しました 文字列の種類と長さの観点から競技場を平準化する。

    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

    次に、スカラー関数を完全に回避するために、上記の2つの関数を組み合わせた単一のインラインテーブル値関数を作成しました。これは、元の記事で実行したかったことです。 (確かにすべてではありません スカラー関数は大規模にひどいものであり、例外はほとんどありません。)

    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

    CASEの変動を避けるために、2つの並べ替えの選択肢のそれぞれに専用のインラインTVFの個別のバージョンも作成しました。 表現しましたが、劇的な影響はまったくありませんでした。

    次に、Steveの2つの関数を作成しました。

    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

    次に、Philの直接クエリをテストリグに配置します(彼のクエリは<をエンコードしていることに注意してください) &lt;として XML解析エラーから保護しますが、>はエンコードしません または& –問題のある文字が含まれる可能性のある文字列から保護する必要がある場合に備えて、プレースホルダーを追加しました):

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

    テストリグは、基本的にこれら2つのクエリと、次の関数呼び出しでした。それらがすべて同じデータを返すことを検証したら、スクリプトにDATEDIFFを散在させました。 出力してテーブルに記録しました:

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

    次に、2つの異なるシステム(1つは8GBのクアッドコア、もう1つは32GBの8コアVM)でパフォーマンステストを実行し、いずれの場合もSQLServer2012とSQLServer2016 CTP 3.2(13.0.900.73)の両方で実行しました。

    結果

    私が観察した結果は、次のグラフにまとめられています。このグラフは、各タイプのクエリの期間をミリ秒単位で示し、アルファベット順と元の順序、4つのサーバーとバージョンの組み合わせ、および各順列に対する一連の15回の実行を示しています。クリックして拡大:

    これは、数値表が過剰に設計されていると見なされているものの、実際に最も効率的なソリューションを生み出したことを示しています(少なくとも期間に関して)。もちろん、これは、元の記事のネストされた関数よりも最近実装した単一のTVFの方が優れていましたが、どちらのソリューションも2つの選択肢を巡回しています。

    詳細については、元の順序を維持するための各マシン、バージョン、クエリタイプの内訳を次に示します。

    …そしてアルファベット順にリストを再組み立てするために:

    これらは、並べ替えの選択が結果にほとんど影響を与えなかったことを示しています。どちらのグラフも実質的に同じです。入力データの形式を考えると、並べ替えをより効率的にするために想像できるインデックスがないため、これは理にかなっています。これは、どのようにスライスしたり、どのようにデータを返したりしても、反復的なアプローチです。しかし、一部の反復アプローチは一般的に他のアプローチよりも悪い可能性があることは明らかであり、必ずしもUDF(または数値の表)を使用してそのようにするわけではありません。

    結論

    SQL Serverにネイティブの分割および連結機能が導入されるまでは、ユーザー定義関数を含め、あらゆる種類の直感的でないメソッドを使用して作業を実行します。一度に1つの文字列を処理している場合、大きな違いは見られません。ただし、データがスケールアップするにつれて、さまざまなアプローチをテストする価値があります(たとえば、上記の方法が最適であることを示唆しているわけではありません。たとえば、CLRも調べていません。このシリーズの他のT-SQLアプローチ)。


    1. SQL Serverで外部キーを作成する方法(T-SQLの例)

    2. MSAccessチームのスペシャルゲストミハルバーと一緒に参加しましょう!

    3. MySQLで重複レコードを検索する

    4. SQL ServerのSYSNAMEデータ型とは何ですか?