クエリの複数の列で同じテーブル値関数を呼び出す最も効率的な方法


8

同じテーブル値関数(TVF)が20列で呼び出されるクエリを調整しようとしています。

最初に行ったのは、スカラー関数をインラインテーブル値関数に変換することでした。

CROSS APPLYクエリの複数の列で同じ関数を実行するために最高のパフォーマンスを発揮する方法を使用していますか?

単純な例:

SELECT   Col1 = A.val
        ,Col2 = B.val
        ,Col3 = C.val
        --do the same for other 17 columns
        ,Col21
        ,Col22
        ,Col23
FROM t
CROSS APPLY
    dbo.function1(Col1) A
CROSS APPLY
    dbo.function1(Col2) B
CROSS APPLY
    dbo.function1(Col3) C
--do the same for other 17 columns

より良い代替案はありますか?

X個の列に対して複数のクエリで同じ関数を呼び出すことができます。

これが関数です:

CREATE FUNCTION dbo.ConvertAmountVerified_TVF
(
    @amt VARCHAR(60)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
    WITH cteLastChar
    AS(
        SELECT LastChar = RIGHT(RTRIM(@amt), 1)
    )
    SELECT
        AmountVerified  = CAST(RET.Y AS NUMERIC(18,2))
    FROM (SELECT 1 t) t
    OUTER APPLY (
        SELECT N =
                CAST(
                    CASE 
                        WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                            THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0)-1
                        WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0) >0
                            THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0)-1
                        WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0) >0
                            THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0)-1
                        ELSE 
                            NULL
                    END
                AS VARCHAR(1))
        FROM
            cteLastChar L
    ) NUM
    OUTER APPLY (
        SELECT N =
            CASE 
                WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                    THEN 0
                WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQRpqrstuvwxy', 0) >0
                    THEN 1
                ELSE 0
            END
        FROM cteLastChar L
    ) NEG
    OUTER APPLY(
        SELECT Amt= CASE
                        WHEN NUM.N IS NULL
                            THEN @amt 
                        ELSE
                            SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + Num.N
                    END
    ) TP
    OUTER APPLY(
        SELECT Y =  CASE
                        WHEN NEG.N = 0
                            THEN (CAST(TP.Amt AS NUMERIC) / 100)
                        WHEN NEG.N = 1
                            THEN (CAST (TP.Amt AS NUMERIC) /100) * -1
                    END
    ) RET
) ;

GO

誰か興味があれば、私が継承したスカラー関数のバージョンを以下に示します。

CREATE   FUNCTION dbo.ConvertAmountVerified 
(
    @amt VARCHAR(50)
)
RETURNS NUMERIC (18,3)  
AS
BEGIN   
    -- Declare the return variable here
    DECLARE @Amount NUMERIC(18, 3);
    DECLARE @TempAmount VARCHAR (50);
    DECLARE @Num VARCHAR(1);
    DECLARE @LastChar VARCHAR(1);
    DECLARE @Negative BIT ;
    -- Get Last Character
    SELECT @LastChar = RIGHT(RTRIM(@amt), 1) ;
    SELECT @Num = CASE @LastChar  collate latin1_general_cs_as
                        WHEN '{'  THEN '0'                                  
                        WHEN 'A' THEN '1'                       
                        WHEN 'B' THEN '2'                       
                        WHEN 'C' THEN '3'                       
                        WHEN 'D' THEN '4'                       
                        WHEN 'E' THEN '5'                       
                        WHEN 'F' THEN '6'                       
                        WHEN 'G' THEN '7'                       
                        WHEN 'H' THEN '8'                       
                        WHEN 'I' THEN '9'                       
                        WHEN '}' THEN '0'   
                        WHEN 'J' THEN '1'
                        WHEN 'K' THEN '2'                       
                        WHEN 'L' THEN '3'                       
                        WHEN 'M' THEN '4'                       
                        WHEN 'N' THEN '5'                       
                        WHEN 'O' THEN '6'                       
                        WHEN 'P' THEN '7'                       
                        WHEN 'Q' THEN '8'                       
                        WHEN 'R' THEN '9'

                        ---ASCII
                        WHEN 'p' Then '0'
                        WHEN 'q' Then '1'
                        WHEN 'r' Then '2'
                        WHEN 's' Then '3'
                        WHEN 't' Then '4'
                        WHEN 'u' Then '5'
                        WHEN 'v' Then '6'
                        WHEN 'w' Then '7'
                        WHEN 'x' Then '8'
                        WHEN 'y' Then '9'

                        ELSE ''

                END 
    SELECT @Negative = CASE @LastChar collate latin1_general_cs_as
                        WHEN '{' THEN 0         

                        WHEN 'A' THEN 0                 
                        WHEN 'B' THEN 0                     
                        WHEN 'C' THEN 0                     
                        WHEN 'D' THEN 0                     
                        WHEN 'E' THEN 0                     
                        WHEN 'F' THEN 0                     
                        WHEN 'G' THEN 0                     
                        WHEN 'H' THEN 0                     
                        WHEN 'I' THEN 0                     
                        WHEN '}' THEN 1 

                        WHEN 'J' THEN 1                     
                        WHEN 'K' THEN 1                     
                        WHEN 'L' THEN 1                     
                        WHEN 'M' THEN 1                 
                        WHEN 'N' THEN 1                     
                        WHEN 'O' THEN 1                     
                        WHEN 'P' THEN 1                     
                        WHEN 'Q' THEN 1                     
                        WHEN 'R' THEN 1

                        ---ASCII
                        WHEN 'p' Then '1'
                        WHEN 'q' Then '1'
                        WHEN 'r' Then '1'
                        WHEN 's' Then '1'
                        WHEN 't' Then '1'
                        WHEN 'u' Then '1'
                        WHEN 'v' Then '1'
                        WHEN 'w' Then '1'
                        WHEN 'x' Then '1'
                        WHEN 'y' Then '1'
                        ELSE 0
                END 
    -- Add the T-SQL statements to compute the return value here
    if (@Num ='')
    begin
    SELECT @TempAmount=@amt;
    end 
    else
    begin
    SELECT @TempAmount = SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + @Num;

    end
    SELECT @Amount = CASE @Negative
                     WHEN 0 THEN (CAST(@TempAmount AS NUMERIC) / 100)
                     WHEN 1 THEN (CAST (@TempAmount AS NUMERIC) /100) * -1
                     END ;
    -- Return the result of the function
    RETURN @Amount

END

サンプルテストデータ:

SELECT dbo.ConvertAmountVerified('00064170')    --  641.700
SELECT * FROM dbo.ConvertAmountVerified_TVF('00064170') --  641.700

SELECT dbo.ConvertAmountVerified('00057600A')   --  5760.010
SELECT * FROM dbo.ConvertAmountVerified_TVF('00057600A')    --  5760.010

SELECT dbo.ConvertAmountVerified('00059224y')   --  -5922.490
SELECT * FROM dbo.ConvertAmountVerified_TVF('00059224y')    --  -5922.490

回答:


8

最初に、望ましい結果を得るための絶対的に最速の方法は以下を行うことであることに言及する必要があります:

  1. 新しい列または新しいテーブルにデータを移行します。
    1. 新しい列のアプローチ:
      1. データ型を{name}_new持つテーブルに新しい列を追加するDECIMAL(18, 3)
      2. 古いVARCHAR列から列へのデータの移行を1回実行DECIMALする
      3. 古い列の名前を {name}_old
      4. 新しい列の名前をちょうどに変更する {name}
    2. 新しいテーブルアプローチ:
      1. データ型を{table_name}_new使用して新しいテーブルを作成するDECIMAL(18, 3)
      2. 現在のテーブルから新しいDECIMALベースのテーブルへのデータの移行を1回実行します。
      3. 古いテーブルの名前を _old
      4. _new新しいテーブルから削除
  2. この方法でエンコードされたデータを挿入しないようにアプリなどを更新する
  3. 1つのリリースサイクルの後、問題がなければ、古い列またはテーブルを削除します。
  4. TVFとUDFをドロップする
  5. これについて二度と話さないでください

それが言われていること:それは主に不必要な重複であるため、そのコードの多くを取り除くことができます。また、出力が正しくない場合や、エラーが発生する場合がある少なくとも2つのバグがあります。そして、それらのバグはOPのコードと同じ結果(エラーを含む)を生成するため、Joeのコードにコピーされました。例えば:

  • これらの値は正しい結果を生成します:

    00062929x
    00021577E
    00000509H
    
  • これらの値は誤った結果を生成します。

    00002020Q
    00016723L
    00009431O
    00017221R
    
  • この値はエラーを生成します:

    00062145}
    anything ending with "}"
    

を使用して3つのバージョンすべてを448,740行と比較すると、すべてSET STATISTICS TIME ON;5000ミリ秒を超える経過時間で実行されました。しかし、CPU時間については、結果は次のとおりです。

  • OPのTVF:7031 ms
  • ジョーのTVF: 3734 ms
  • ソロモンのTVF:1407ミリ秒

設定:データ

次の例では、テーブルを作成してデータを入力します。これにより、SQL Server 2017を実行しているすべてのシステムで同じデータセットが作成されます。これらのシステムには内の同じ行が含まれるためspt_valuesです。これは、ランダムに生成されたデータがシステム全体、またはサンプルデータが再生成される場合は同じシステムでのテスト間のタイミングの違いを考慮に入れるため、システムでテストする他の人々の比較の基礎を提供するのに役立ちます。私はJoeと同じ3列のテーブルから始めましたが、質問のサンプル値をテンプレートとして使用して、さまざまな数値に可能な末尾文字オプション(末尾文字なしを含む)を追加しました。これが列に照合順序を強制した理由でもあります。バイナリ照合順序インスタンスを使用して、使用の影響を不当に無効にするという事実を望んでいません。COLLATE TVFで別の照合順序を強制するキーワード)。

唯一の違いは、テーブル内の行の順序です。

USE [tempdb];
SET NOCOUNT ON;

CREATE TABLE dbo.TestVals
(
  [TestValsID] INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
  [Col1] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL,
  [Col2] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL,
  [Col3] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL
);

;WITH cte AS
(
  SELECT (val.[number] + tmp.[blah]) AS [num]
  FROM [master].[dbo].[spt_values] val
  CROSS JOIN (VALUES (1), (7845), (0), (237), (61063), (999)) tmp(blah)
  WHERE val.[number] BETWEEN 0 AND 1000000
)
INSERT INTO dbo.TestVals ([Col1], [Col2], [Col3])
  SELECT FORMATMESSAGE('%08d%s', cte.[num], tab.[col]) AS [Col1],
       FORMATMESSAGE('%08d%s', ((cte.[num] + 2) * 2), tab.[col]) AS [Col2],
       FORMATMESSAGE('%08d%s', ((cte.[num] + 1) * 3), tab.[col]) AS [Col3]
  FROM    cte
  CROSS JOIN (VALUES (''), ('{'), ('A'), ('B'), ('C'), ('D'), ('E'), ('F'),
              ('G'), ('H'), ('I'), ('}'), ('J'), ('K'), ('L'), ('M'), ('N'),
              ('O'), ('P'), ('Q'), ('R'), ('p'), ('q'), ('r'), ('s'), ('t'),
              ('u'), ('v'), ('w'), ('x'), ('y')) tab(col)
  ORDER BY NEWID();
-- 463698 rows

セットアップ:TVF

GO
CREATE OR ALTER FUNCTION dbo.ConvertAmountVerified_Solomon
(
    @amt VARCHAR(50)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN

    WITH ctePosition AS
    (
        SELECT CHARINDEX(RIGHT(RTRIM(@amt), 1) COLLATE Latin1_General_100_BIN2,
                             '{ABCDEFGHI}JKLMNOPQRpqrstuvwxy') AS [Value]
    ),
    cteAppend AS
    (
        SELECT pos.[Value] AS [Position],
               IIF(pos.[Value] > 0,
                      CHAR(48 + ((pos.[Value] - 1) % 10)),
                      '') AS [Value]
        FROM   ctePosition pos
    )
    SELECT (CONVERT(DECIMAL(18, 3),
                    IIF(app.[Position] > 0,
                           SUBSTRING(RTRIM(@amt), 1, LEN(@amt) - 1) + app.[Value],
                           @amt))
                        / 100. )
                    * IIF(app.[Position] > 10, -1., 1.) AS [AmountVerified]
    FROM   cteAppend app;
GO

ご注意ください:

  1. _BIN2言語ルールを考慮する必要がないため、大文字と小文字を区別する照合よりも高速なバイナリ(つまり)照合を使用しました。
  2. 本当に重要な唯一のことは、アルファ文字と2つの中括弧のリスト内の右端の文字の位置(つまり、「インデックス」)です。操作的に行われるすべてのことは、文字自体の値よりもその位置から派生​​します。
  3. 私はから行くには十分な理由があった場合を除きOPによって書き換えられたオリジナルのUDFに示すように、入力パラメータと戻り値のデータ型を使用VARCHAR(50)するにはVARCHAR(60)、およびからNUMERIC (18,3)NUMERIC (18,2)(正当な理由は、「彼らは間違っていた」だろう)、そして私が固執するだろう元の署名/タイプ。
  4. :私は3数値リテラル/定数の末尾にピリオド/小数点を追加した100.-1.1.。これはこのTVFの元のバージョン(この回答の履歴にあります)にはありませんでしたCONVERT_IMPLICITが、XML実行プランにいくつかの呼び出しがあることに気づきました(これ100はですINTが、操作はNUMERIC/である必要があるDECIMALため)。 。
  5. 私が使用して文字列の文字を作成するCHAR()機能をよりむしろ数(例えばの文字列バージョンを渡す'2'に)CONVERT(私はもともと歴史の中で、再び、何をやっていたた)機能。これは非常にわずかに速いようです。ほんの数ミリ秒ですが、まだです。

テスト

}OPとJoeのTVFでエラーが発生したため、末尾がの行を除外する必要があったことに注意してください。私のコードは}正しく処理していますが、3つのバージョン間でテストされている行と一貫性を保つ必要がありました。これが、セットアップクエリによって生成される行の数が、テストされている行数のテスト結果の上に示した数よりもわずかに多い理由です。

SET STATISTICS TIME ON;

DECLARE @Dummy DECIMAL(18, 3);
SELECT --@Dummy =  -- commented out = results to client; uncomment to not return results
cnvrtS.[AmountVerified]
FROM  dbo.TestVals vals
CROSS APPLY dbo.ConvertAmountVerified_Solomon(vals.[Col1]) cnvrtS
WHERE RIGHT(vals.[Col1], 1) <> '}'; -- filter out rows that cause error in O.P.'s code

SET STATISTICS TIME OFF;
GO

のコメントを--@Dummy =外すと、CPU時間はわずかに短くなり、3つのTVF間のランキングは同じです。しかし興味深いことに、変数のコメントを外すと、ランキングが少し変化します。

  • ジョーのTVF: 3295ミリ秒
  • OPのTVF:2240ミリ秒
  • ソロモンのTVF:1203 ms

OPのコードがこのシナリオでそれほど優れたパフォーマンスを発揮する理由はわかりませんが(私のコードとJoeのコードはわずかに改善されただけです)、多くのテストで一貫しているように見えます。いいえ、実行プランの違いを調査する時間がないため、違いは確認しませんでした。

より速く

私は代替アプローチのテストを完了しました、そしてそれは上に示されているものにわずかですが明確な改善を提供します。新しいアプローチはSQLCLRを使用しており、より適切に拡張できるようです。2番目の列をクエリに追加すると、T-SQLのアプローチが時間的に2倍になることがわかりました。ただし、SQLCLRスカラーUDFを使用して列を追加すると、時間が増加しましたが、単一列のタイミングと同じではありませんでした。タイミングが(CPU時間ではなく経過時間)だったため、SQLCLRメソッドの呼び出しに初期オーバーヘッド(アプリドメインおよびアプリドメインへのアセンブリの初期読み込みのオーバーヘッドに関連付けられていない)がある可能性があります。

  • 1列:1018ミリ秒
  • 2列:1750-1800 ms
  • 3列:2500-2600 ms

したがって、(結果セットを返さずに変数にダンプする)タイミングに200ミリ秒-250ミリ秒のオーバーヘッドがあり、インスタンス時間あたり750ミリ秒-800ミリ秒になる可能性があります。CPUタイミングは、UDFの1、2、および3つのインスタンスで、それぞれ950ミリ秒、1750ミリ秒、2400ミリ秒でした。

C#コード

using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public class Transformations
{
    private const string _CHARLIST_ = "{ABCDEFGHI}JKLMNOPQRpqrstuvwxy";

    [SqlFunction(IsDeterministic = true, IsPrecise = true,
        DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None)]
    public static SqlDouble ConvertAmountVerified_SQLCLR(
        [SqlFacet(MaxSize = 50)] SqlString Amt)
    {
        string _Amount = Amt.Value.TrimEnd();

        int _LastCharIndex = (_Amount.Length - 1);
        int _Position = _CHARLIST_.IndexOf(_Amount[_LastCharIndex]);

        if (_Position >= 0)
        {
            char[] _TempAmount = _Amount.ToCharArray();
            _TempAmount[_LastCharIndex] = char.ConvertFromUtf32(48 + (_Position % 10))[0];
            _Amount = new string(_TempAmount);
        }

        decimal _Return = decimal.Parse(_Amount) / 100M;

        if (_Position > 9)
        {
            _Return *= -1M;
        }

        return new SqlDouble((double)_Return);
    }
}

元々SqlDecimalは戻り値の型として使用していましたが、SqlDouble/ とは対照的に、それを使用するとパフォーマンスが低下しFLOATます。時々FLOATには問題があります(不正確なタイプのため)が、次のクエリを介してT-SQL TVFに対して検証しましたが、違いは検出されませんでした。

SELECT cnvrtS.[AmountVerified],
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col1])
FROM   dbo.TestVals vals
CROSS APPLY dbo.ConvertAmountVerified_Solomon(vals.[Col1]) cnvrtS
WHERE  cnvrtS.[AmountVerified] <> dbo.ConvertAmountVerified_SQLCLR(vals.[Col1]);

テスト

SET STATISTICS TIME ON;

DECLARE @Dummy DECIMAL(18, 3), @Dummy2 DECIMAL(18, 3), @Dummy3 DECIMAL(18, 3);
SELECT @Dummy = 
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col1])
              , @Dummy2 =
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col2])
              , @Dummy3 =
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col3])
FROM  dbo.TestVals vals
WHERE RIGHT(vals.[Col1], 1) <> '}';

SET STATISTICS TIME OFF;

これをありがとう。私のデータに対して関数をテストします。変更を確認してさらに高速化し、データをテストすることを楽しみにしています。
Mazhar 2018年

1
@Mazhar受け入れてくれてありがとう:-) しかし、私は代替アプローチでのテストを完了し、私がすでに持っていたものよりわずかに速いことがわかりました。SQLCLRを使用しますが、拡張性が向上します。また、スカラーUDFに戻ったので、作業が少し簡単になりました(つまり、CROSS APPLYs が不要)。
ソロモンルツキー

おそらく、SQLCLRメソッドの呼び出しに初期オーバーヘッドがいくつかあります(アプリケーションドメインおよびアプリケーションドメインへのアセンブリの初期ロードのオーバーヘッドに関連付けられていません)」-オーバーヘッドはJITコンパイルである可能性があることを示唆していましたが、最初の実行でのみ発生するためです。しかし、C#コンソールアプリでコードのプロファイルを作成したところ、JITコンパイルに10ミリ秒しかかかりませんでした。静的メソッドはJITされるのに0.3秒しかかかりませんでした。しかし、SQLCLRについては何も知りません。そのため、私が知っているよりも多くのコードが関係している可能性があります。
ジョシュダーネル、

1
@ jadarnel27調査にご協力いただきありがとうございます。何かの許可チェックかもしれませんね。クエリプランの生成/検証に関連する何か。
ソロモンルツキー

4

最初に、いくつかのテストデータをテーブルにスローします。私はあなたの実際のデータがどのように見えるのか分かりませんので、私は連続した整数を使用しました:

CREATE TABLE APPLY_FUNCTION_TO_ME (
    COL1 VARCHAR(60),
    COL2 VARCHAR(60),
    COL3 VARCHAR(60)
);

INSERT INTO APPLY_FUNCTION_TO_ME WITH (TABLOCK)
SELECT RN, RN, RN
FROM (
    SELECT CAST(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS VARCHAR(60)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

結果セットをオフにしてすべての行を選択すると、ベースラインが提供されます。

-- CPU time = 1359 ms,  elapsed time = 1434 ms.
SELECT COL1 FROM dbo.APPLY_FUNCTION_TO_ME

関数呼び出しを使用した同様のクエリにさらに時間がかかる場合は、関数のオーバーヘッドに関する大まかな見積もりがあります。TVFをそのまま呼び出すと次のようになります。

-- CPU time = 41703 ms,  elapsed time = 41899 ms.
SELECT t1.AmountVerified
FROM dbo.APPLY_FUNCTION_TO_ME
CROSS APPLY dbo.ConvertAmountVerified_TVF (COL1) t1
OPTION (MAXDOP 1);

したがって、この関数は、650万行に対して約40秒のCPU時間を必要とします。これに20を掛けると、CPU時間は800秒になります。私はあなたの関数コードに2つのことに気づきました:

  1. の不必要な使用OUTER APPLYCROSS APPLY同じ結果が得られ、このクエリでは、不必要な結合がたくさん発生するのを回避できます。それは少しの時間を節約できます。ほとんどの場合、完全なクエリが並列処理されるかどうかに依存します。データやクエリについて何も知らないので、でテストしていMAXDOP 1ます。その場合、私はの方がよいでしょうCROSS APPLY

  2. CHARINDEX一致する値の小さなリストに対して1つの文字を検索するだけの場合、多くの呼び出しがあります。ASCII()関数と簡単な数学を使用して、すべての文字列比較を回避できます。

関数を作成する別の方法を次に示します。

CREATE OR ALTER FUNCTION dbo.ConvertAmountVerified_TVF3
(
    @amt VARCHAR(60)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
    WITH cteLastChar
    AS(
        SELECT LastCharASCIICode =  ASCII(RIGHT(RTRIM(@amt), 1) COLLATE Latin1_General_CS_AS)
    )
    SELECT
        AmountVerified  = CAST(RET.Y AS NUMERIC(18,2))
    FROM cteLastChar
    CROSS APPLY (
        SELECT N =
                CAST(
                    CASE 
                        --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                        --    THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0)-1
                        WHEN LastCharASCIICode = 123 THEN 0
                        WHEN LastCharASCIICode BETWEEN 65 AND 73 THEN LastCharASCIICode - 64
                        WHEN LastCharASCIICode = 125 THEN 10

                        --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0) >0
                        --    THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0)-1
                        WHEN LastCharASCIICode BETWEEN 74 AND 82 THEN LastCharASCIICode - 74

                        --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0) >0
                        --    THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0)-1
                        WHEN LastCharASCIICode BETWEEN 112 AND 121 THEN LastCharASCIICode - 112
                        ELSE 
                            NULL
                    END
                AS VARCHAR(1))
        --FROM
        --    cteLastChar L
    ) NUM
    CROSS APPLY (
        SELECT N =
            CASE 
                --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                WHEN LastCharASCIICode = 123 OR LastCharASCIICode = 125 OR LastCharASCIICode BETWEEN 65 AND 73
                    THEN 0

                --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQRpqrstuvwxy', 0) >0
                WHEN LastCharASCIICode BETWEEN 74 AND 82 OR LastCharASCIICode BETWEEN 112 AND 121
                    THEN 1
                ELSE 0
            END
        --FROM cteLastChar L
    ) NEG
    CROSS APPLY(
        SELECT Amt= CASE
                        WHEN NUM.N IS NULL
                            THEN @amt 
                        ELSE
                            SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + Num.N
                    END
    ) TP
    CROSS APPLY(
        SELECT Y =  CASE
                        WHEN NEG.N = 0
                            THEN (CAST(TP.Amt AS NUMERIC) / 100)
                        WHEN NEG.N = 1
                            THEN (CAST (TP.Amt AS NUMERIC) /100) * -1
                    END
    ) RET
) ;

GO

私のマシンでは、新しい機能が大幅に高速化されています。

-- CPU time = 7813 ms,  elapsed time = 7876 ms.
SELECT t1.AmountVerified
FROM dbo.APPLY_FUNCTION_TO_ME
CROSS APPLY dbo.ConvertAmountVerified_TVF3 (COL1) t1
OPTION (MAXDOP 1);

おそらくいくつかの追加の最適化も同様に利用可能ですが、私の直感ではそれらはそれほど多くはならないでしょう。あなたのコードが何をしているかに基づいて、どういうわけか別の方法で関数を呼び出すことによって、どのようにさらなる改善が見られるかわかりません。これは単なる文字列操作の集まりです。行ごとに関数を20回呼び出すと、1回よりも遅くなりますが、定義は既にインライン化されています。


これをありがとう。複数の列でのTVFの実行はインライン関数のように動作すると「定義はすでにインライン化されています」と言っていますか?
Mazhar

私のデータに対して関数をテストします。
Mazhar 2018年

2

以下を使用してみてください

-- Get Last Character
SELECT @LastChar = RIGHT(RTRIM(@amt), 1) collate latin1_general_cs_as;

DECLARE @CharPos int=NULLIF(CHARINDEX(@LastChar,'{ABCDEFGHI}JKLMNOPQRpqrstuvwxy'),0)-1
SET @Num = ISNULL(@CharPos%10,''); 
SET @Negative = IIF(@CharPos>9,1,0);

代わりに

SELECT @Num =
    CASE @LastChar  collate latin1_general_cs_as
        WHEN '{'  THEN '0'
...

SELECT @Negative =
    CASE @LastChar collate latin1_general_cs_as
        WHEN '{' THEN 0
...

補助テーブルを使用する1つのバリアント

-- auxiliary table
CREATE TABLE LastCharLink(
  LastChar varchar(1) collate latin1_general_cs_as NOT NULL,
  Num varchar(1) NOT NULL,
  Prefix varchar(1) NOT NULL,
CONSTRAINT PK_LastCharLink PRIMARY KEY(LastChar)
)

INSERT LastCharLink(LastChar,Num,Prefix)VALUES
('{','0',''),
('A','1',''),
('B','2',''),
('C','3',''),
('D','4',''),
('E','5',''),
('F','6',''), 
('G','7',''), 
('H','8',''), 
('I','9',''), 
('}','0','-'), 
('J','1','-'),
('K','2','-'),
('L','3','-'),
('M','4','-'),
('N','5','-'),
('O','6','-'),
('P','7','-'),
('Q','8','-'),
('R','9','-'),                
('p','0','-'),
('q','1','-'),
('r','2','-'),
('s','3','-'),
('t','4','-'),
('u','5','-'),
('v','6','-'),
('w','7','-'),
('x','8','-'),
('y','9','-')

テストクエリ

CREATE TABLE #TestAmounts(Amt varchar(10))
INSERT #TestAmounts(Amt)VALUES('00064170'),('00057600A'),('00066294R'),('00059224}'),('00012345p')

SELECT
  *,
  CAST( -- step 5 - final cast
      CAST( -- step 3 - convert to number
          CONCAT( -- step 2 - add a sign and an additional number
              l.Prefix,
              LEFT(RTRIM(a.Amt),LEN(RTRIM(a.Amt))-IIF(l.LastChar IS NULL,0,1)), -- step 1 - remove last char
              l.Num
            )
          AS numeric(18,3)
        )/100 -- step 4 - divide
      AS numeric(18,3)
    ) ResultAmt
FROM #TestAmounts a
LEFT JOIN LastCharLink l ON RIGHT(RTRIM(a.Amt),1) collate latin1_general_cs_as=l.LastChar

DROP TABLE #TestAmounts

バリアントとして、一時補助テーブル#LastCharLinkまたは変数テーブルを使用することもできます@LastCharLink(ただし、実際のテーブルや一時テーブルよりも遅くなる場合があります)

DECLARE @LastCharLink TABLE(
  LastChar varchar(1) collate latin1_general_cs_as NOT NULL,
  Num varchar(1) NOT NULL,
  Prefix varchar(1) NOT NULL,
PRIMARY KEY(LastChar)
)

INSERT LastCharLink(LastChar,Num,Prefix)VALUES
('{','0',''),
('A','1',''),
('B','2',''),
('C','3',''),
('D','4',''),
('E','5',''),
...

そしてそれを

FROM #TestAmounts a
LEFT JOIN #LastCharLink l ON ...

または

FROM #TestAmounts a
LEFT JOIN @LastCharLink l ON ...

次に、単純なインライン関数を作成して、すべての変換を追加することもできます

CREATE FUNCTION NewConvertAmountVerified(
  @Amt varchar(50),
  @LastChar varchar(1),
  @Num varchar(1),
  @Prefix varchar(1)
)
RETURNS numeric(18,3)
AS
BEGIN
  RETURN CAST( -- step 3 - convert to number
              CONCAT( -- step 2 - add a sign and an additional number
                  @Prefix,
                  LEFT(@Amt,LEN(@Amt)-IIF(@LastChar IS NULL,0,1)), -- step 1 - remove last char
                  @Num
                )
              AS numeric(18,3)
            )/100 -- step 4 - divide
END
GO

そして、この関数を次のように使用します

CREATE TABLE #TestAmounts(Amt varchar(10))
INSERT #TestAmounts(Amt)VALUES('00064170'),('00057600A'),('00066294R'),('00059224}'),('00012345p')

SELECT
  *,
  -- you need to use `RTRIM` here
  dbo.NewConvertAmountVerified(RTRIM(a.Amt),l.LastChar,l.Num,l.Prefix) ResultAmt
FROM #TestAmounts a
LEFT JOIN LastCharLink l ON RIGHT(RTRIM(a.Amt),1) collate latin1_general_cs_as=l.LastChar

DROP TABLE #TestAmounts

回答を更新しました。必要なことを行うには、補助テーブルを使用してください。このバリアントはより高速になると思います。

もう一度回答を更新しました。今ではのPrefix代わりに使用していますDivider

2

または、永続テーブルを1つ作成することもできます。これは1回限りの作成です。

CREATE TABLE CharVal (
    charactor CHAR(1) collate latin1_general_cs_as NOT NULL
    ,positiveval INT NOT NULL
    ,negativeval INT NOT NULL
    ,PRIMARY KEY (charactor)
    )

insert into CharVal (charactor,positiveval,negativeval) VALUES

 ( '{' ,'0', 0 ),( 'A' ,'1', 0 ) ,( 'B' ,'2', 0 ) ,( 'C' ,'3', 0 ) ,( 'D' ,'4', 0 )       
                         ,( 'E' ,'5', 0 )  ,( 'F' ,'6', 0 ) ,( 'G' ,'7', 0 ) ,( 'H' ,'8', 0 )       
,( 'I' ,'9', 0 ),( '}' ,'0', 1 ),( 'J' ,'1', 1  ),( 'K' ,'2', 1 ) ,( 'L' ,'3', 1 ) ,( 'M' ,'4', 1 )       
,( 'N' ,'5', 1 )  ,( 'O' ,'6', 1 )  ,( 'P' ,'7', 1 )  ,( 'Q' ,'8', 1 )  ,( 'R' ,'9', 1  )
---ASCII
,( 'p' , '0', '1'),( 'q' , '1', '1'),( 'r' , '2', '1'),( 's' , '3', '1')
,( 't' , '4', '1'),( 'u' , '5', '1'),( 'v' , '6', '1'),( 'w' , '7', '1')
,( 'x' , '8', '1'),( 'y' , '9', '1')

--neg
('{' ,2, 0) ,('A' ,2, 0) ,('B' ,2, 0)  ,('C' ,2, 0) ,('D' ,2, 0)                    
,('E' ,2, 0),('F' ,2, 0)  ,('G' ,2, 0) ,('H' ,2, 0) ,('I' ,2, 0) ,('}' ,2, 1)
,('J' ,2, 1) ,('K' ,2, 1) ,('L' ,2, 1) ,('M' ,2, 1) ,('N' ,2, 1)                    
,('O' ,2, 1)  ,('P' ,2, 1)  ,('Q' ,2, 1) ,('R' ,2, 1)
  ---ASCII
,( 'p' ,2, '1'),( 'q' ,2, '1')
,( 'r' ,2, '1'),( 's' ,2, '1')
,( 't' ,2, '1'),( 'u' ,2, '1')
,( 'v' ,2, '1'),( 'w' ,2, '1')
,( 'x' ,2, '1'),( 'y' ,2, '1')

その後TVF

ALTER FUNCTION dbo.ConvertAmountVerified_TVFHarsh (@amt VARCHAR(60))
RETURNS TABLE
    WITH SCHEMABINDING
AS
RETURN (
        WITH MainCTE AS (
                SELECT TOP 1 
                Amt = CASE 
                        WHEN positiveval IS NULL
                            THEN @amt
                        ELSE SUBSTRING(RTRIM(@amt), 1, LEN(@amt) - 1) + positiveval
                        END
                    ,negativeval
                FROM (
                    SELECT positiveval
                        ,negativeval negativeval
                        ,1 sortorder
                    FROM dbo.CharVal WITH (NOLOCK)
                    WHERE (charactor = RIGHT(RTRIM(@amt), 1))

                    UNION ALL

                    SELECT NULL
                        ,0
                        ,0
                    ) t4
                ORDER BY sortorder DESC
                )

        SELECT AmountVerified = CASE 
                WHEN negativeval = 0
                    THEN (CAST(TP.Amt AS NUMERIC) / 100)
                WHEN negativeval = 1
                    THEN (CAST(TP.Amt AS NUMERIC) / 100) * - 1
                END
        FROM MainCTE TP
        );
GO

@Joeの例から、

-30秒かかります

SELECT t1.AmountVerified
FROM dbo.APPLY_FUNCTION_TO_ME
CROSS APPLY dbo.ConvertAmountVerified_TVFHarsh (COL1) t1
OPTION (MAXDOP 1);

可能であれば、金額はUIレベルでもフォーマットできます。これが最良の選択肢です。それ以外の場合は、元のクエリを共有することもできます。または、可能であれば、フォーマットされた値もテーブルに保持します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.