Die effizienteste Methode zum Aufrufen derselben Tabellenwertfunktion für mehrere Spalten in einer Abfrage

8

Ich versuche, eine Abfrage zu optimieren, bei der dieselbe Tabellenwertfunktion (TVF) für 20 Spalten aufgerufen wird.

Als erstes habe ich die Skalarfunktion in eine Inline-Tabellenwertfunktion konvertiert.

Verwenden Sie CROSS APPLYdie leistungsstärkste Methode, um dieselbe Funktion für mehrere Spalten in einer Abfrage auszuführen?

Ein vereinfachtes Beispiel:

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

Gibt es bessere Alternativen?

Dieselbe Funktion kann in mehreren Abfragen für die X-Anzahl von Spalten aufgerufen werden.

Hier ist die Funktion:

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

Hier ist die Skalarfunktionsversion, die ich geerbt habe, wenn jemand interessiert ist:

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

Beispieltestdaten:

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

Antworten:

8

ZUERST: Es sollte erwähnt werden, dass die absolut schnellste Methode, um die gewünschten Ergebnisse zu erzielen, Folgendes ist:

  1. Migrieren Sie Daten entweder in neue Spalten oder sogar in eine neue Tabelle:
    1. Neuer Spaltenansatz:
      1. Fügen Sie {name}_newder Tabelle mit dem DECIMAL(18, 3)Datentyp neue Spalten hinzu
      2. Führen Sie eine einmalige Migration der Daten von den alten VARCHARSpalten in die DECIMALSpalten durch
      3. Benennen Sie die alten Spalten in um {name}_old
      4. benenne neue Spalten so um, dass sie gerecht sind {name}
    2. Neuer Tabellenansatz:
      1. Erstellen Sie eine neue Tabelle {table_name}_newmit dem DECIMAL(18, 3)Datentyp
      2. Führen Sie eine einmalige Migration der Daten von der aktuellen Tabelle in eine neue DECIMALTabelle durch.
      3. benenne alte Tabelle um in _old
      4. _newaus neuer Tabelle entfernen
  2. Aktualisieren Sie die App usw., um niemals auf diese Weise codierte Daten einzufügen
  3. Wenn nach einem Release-Zyklus keine Probleme auftreten, löschen Sie alte Spalten oder Tabellen
  4. TVFs und UDF löschen
  5. Sprich nie wieder davon!

DAS WIRD GESAGT: Sie können einen Großteil dieses Codes loswerden, da es sich weitgehend um unnötige Vervielfältigung handelt. Außerdem gibt es mindestens zwei Fehler, die dazu führen, dass die Ausgabe manchmal falsch ist oder manchmal einen Fehler auslöst. Und diese Fehler wurden in Joes Code kopiert, da sie dieselben Ergebnisse (einschließlich des Fehlers) wie der OP-Code liefern. Zum Beispiel:

  • Diese Werte führen zu einem korrekten Ergebnis:

    00062929x
    00021577E
    00000509H
  • Diese Werte führen zu einem falschen Ergebnis:

    00002020Q
    00016723L
    00009431O
    00017221R
  • Dieser Wert erzeugt einen Fehler:

    00062145}
    anything ending with "}"

Beim Vergleich aller drei Versionen mit 448.740 Zeilen, die mit verwendet wurden SET STATISTICS TIME ON;, liefen alle in etwas mehr als 5000 ms verstrichener Zeit. Für die CPU-Zeit waren die Ergebnisse jedoch:

  • OPF des OP: 7031 ms
  • Joes TVF: 3734 ms
  • Solomons TVF: 1407 ms

SETUP: DATEN

Im Folgenden wird eine Tabelle erstellt und gefüllt. Dadurch sollte auf allen Systemen, auf denen SQL Server 2017 ausgeführt wird, derselbe Datensatz erstellt werden, da sie dieselben Zeilen enthalten spt_values. Dies hilft dabei, eine Vergleichsbasis zwischen anderen Personen zu schaffen, die auf ihrem System testen, da zufällig generierte Daten die Zeitunterschiede zwischen Systemen oder sogar zwischen Tests auf demselben System berücksichtigen würden, wenn die Probendaten neu generiert werden. Ich habe mit der gleichen 3-Spalten-Tabelle wie Joe begonnen, aber die Beispielwerte aus der Frage als Vorlage verwendet, um eine Vielzahl von numerischen Werten zu erstellen, die an jede der möglichen Optionen für nachfolgende Zeichen angehängt sind (einschließlich ohne nachfolgendes Zeichen). Dies ist auch der Grund, warum ich die Kollatierung für die Spalten erzwungen habe: Ich wollte nicht, dass ich eine binäre Kollatierungsinstanz verwende, um den Effekt der Verwendung von ungerechtfertigt zu negierenCOLLATE Schlüsselwort, um eine andere Sortierung in der TVF zu erzwingen).

Der einzige Unterschied besteht in der Reihenfolge der Zeilen in der Tabelle.

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

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

Bitte beachten Sie:

  1. Ich habe eine binäre (dh _BIN2) Kollatierung verwendet, die schneller ist als eine Kollatierung, bei der zwischen Groß- und Kleinschreibung unterschieden wird, da keine sprachlichen Regeln berücksichtigt werden müssen.
  2. Das einzige, was wirklich zählt, ist die Position (dh der "Index") des am weitesten rechts stehenden Zeichens in der Liste der Alpha-Zeichen plus der beiden geschweiften Klammern. Alles, was operativ gemacht wird, wird mehr von dieser Position abgeleitet als vom Wert des Charakters selbst.
  3. Ich habe die Eingabeparameter- und Rückgabewert-Datentypen verwendet, wie in der ursprünglichen UDF angegeben, die vom OP neu geschrieben wurde. Wenn es keinen guten Grund gab, von VARCHAR(50)nach VARCHAR(60)und von NUMERIC (18,3)nach zu NUMERIC (18,2)wechseln (guter Grund wäre "sie waren falsch"), würde ich bleiben mit der ursprünglichen Unterschrift / Typen.
  4. Ich habe eine Zeit / Dezimalpunkt bis zum Ende des 3 Zahlenliterale / Konstanten: 100., -1., und 1.. Dies war nicht in meiner Originalversion dieser TVF (in der Geschichte dieser Antwort), aber ich bemerkte einige CONVERT_IMPLICITAufrufe im XML-Ausführungsplan (da dies 100eine ist, INTaber die Operation muss NUMERIC/ sein DECIMAL), also habe ich mich vorher darum gekümmert .
  5. Ich erstelle ein Zeichenfolgenzeichen mit der CHAR()Funktion, anstatt eine Zeichenfolgenversion einer Zahl (z. B. '2') an eine CONVERTFunktion zu übergeben (was ich ursprünglich wieder im Verlauf getan habe). Dies scheint etwas schneller zu sein. Nur ein paar Millisekunden, aber immer noch.

PRÜFUNG

Bitte beachten Sie, dass ich Zeilen herausfiltern musste, die mit enden, }da dies zu Fehlern bei den TVFs von OP und Joe führte. Während mein Code das }korrekt handhabt , wollte ich mit den Zeilen übereinstimmen, die in den drei Versionen getestet wurden. Aus diesem Grund ist die Anzahl der von der Setup-Abfrage generierten Zeilen geringfügig höher als die Anzahl, die ich über den Testergebnissen für die Anzahl der getesteten Zeilen angegeben habe.

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

Die CPU-Zeit ist beim --@Dummy =Kommentieren der nur geringfügig kürzer, und die Rangfolge unter den 3 TVFs ist dieselbe. Interessanterweise ändern sich die Rangfolgen beim Kommentieren der Variablen ein wenig:

  • Joes TVF: 3295 ms
  • OPF des OP: 2240 ms
  • Solomons TVF: 1203 ms

Ich bin mir nicht sicher, warum der OP-Code in diesem Szenario so viel besser abschneiden würde (während sich mein und Joes Code nur geringfügig verbessert haben), aber er scheint in vielen Tests konsistent zu sein. Und nein, ich habe mir die Unterschiede im Ausführungsplan nicht angesehen, da ich keine Zeit habe, dies zu untersuchen.

AUCH SCHNELLER

Ich habe den Test des alternativen Ansatzes abgeschlossen und er bietet eine leichte, aber deutliche Verbesserung gegenüber dem oben gezeigten. Der neue Ansatz verwendet SQLCLR und scheint besser zu skalieren. Ich habe festgestellt, dass sich der T-SQL-Ansatz beim Hinzufügen in der zweiten Spalte zur Abfrage zeitlich verdoppelt. Beim Hinzufügen zusätzlicher Spalten mithilfe einer skalaren SQLCLR-UDF stieg die Zeit jedoch an, jedoch nicht um den gleichen Betrag wie beim Timing einzelner Spalten. Möglicherweise entsteht beim Aufrufen der SQLCLR-Methode ein anfänglicher Overhead (der nicht mit dem Overhead des anfänglichen Ladens der App-Domäne und der Assembly in die App-Domäne verbunden ist), da die Zeitabläufe (verstrichene Zeit, nicht CPU-Zeit) waren:

  • 1 Spalte: 1018 ms
  • 2 Spalten: 1750 - 1800 ms
  • 3 Spalten: 2500 - 2600 ms

Es ist also möglich, dass das Timing (Dumping in eine Variable, keine Rückgabe der Ergebnismenge) einen Overhead von 200 ms - 250 ms und dann 750 ms - 800 ms pro Instanzzeit hat. Die CPU-Timings waren: 950 ms, 1750 ms und 2400 ms für 1, 2 bzw. 3 Instanzen der UDF.

C # CODE

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

Ich habe ursprünglich SqlDecimalals Rückgabetyp verwendet, aber es gibt eine Leistungsstrafe für die Verwendung im Gegensatz zu SqlDouble/ FLOAT. Manchmal hat FLOAT Probleme (da es sich um einen ungenauen Typ handelt), aber ich habe die folgende Abfrage anhand der T-SQL-TVF überprüft und keine Unterschiede festgestellt:

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

PRÜFUNG

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;
Solomon Rutzky
quelle
Danke dafür. Ich werde Ihre Funktion anhand meiner Daten testen. Wir freuen uns darauf, Ihre Änderungen zu sehen, um sie noch schneller zu machen und Daten zu testen.
Mazhar
1
@ Mazhar Danke fürs Akzeptieren :-). Ich habe jedoch meine Tests mit dem alternativen Ansatz abgeschlossen und festgestellt, dass er etwas schneller ist als das, was ich hier bereits hatte. Es verwendet SQLCLR, skaliert aber besser. Es ist auch wieder eine skalare UDF, die ein wenig einfacher zu bearbeiten ist (dh das CROSS APPLYs nicht benötigt ).
Solomon Rutzky
" Möglicherweise ist das Aufrufen der SQLCLR-Methode mit einem anfänglichen Aufwand verbunden (der nicht mit dem Aufwand für das anfängliche Laden der App-Domäne und der Assembly in die App-Domäne verbunden ist). " - Ich wollte vorschlagen, dass der Aufwand eine JIT-Kompilierung sein könnte. da es nur beim ersten Durchlauf angetroffen wird. Aber ich habe Ihren Code in einer C # -Konsolen-App profiliert und es wurden nur 10 ms JIT-Kompilierung ausgeführt. Die statische Methode benötigte speziell nur 0,3 ms, um JIT-fähig zu sein. Aber ich weiß nichts über SQLCLR, also ist vielleicht mehr Code involviert, als mir bekannt ist.
Josh Darnell
1
@ jadarnel27 Danke, dass du bei der Untersuchung geholfen hast. Ich denke, es könnte eine Erlaubnisprüfung von etwas sein. Etwas im Zusammenhang mit dem Generieren / Validieren des Abfrageplans.
Solomon Rutzky
4

Ich werde zunächst einige Testdaten in eine Tabelle werfen. Ich habe keine Ahnung, wie Ihre realen Daten aussehen, also habe ich nur sequentielle Ganzzahlen verwendet:

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;

Wenn Sie alle Zeilen mit deaktivierten Ergebnismengen auswählen, erhalten Sie eine Basislinie:

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

Wenn eine ähnliche Abfrage mit dem Funktionsaufruf länger dauert, haben wir eine grobe Schätzung des Overheads der Funktion. Folgendes bekomme ich, wenn ich Ihre TVF so anrufe, wie sie ist:

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

Die Funktion benötigt also ungefähr 40 Sekunden CPU-Zeit für 6,5 Millionen Zeilen. Multiplizieren Sie dies mit 20 und es sind 800 Sekunden CPU-Zeit. Ich habe zwei Dinge in Ihrem Funktionscode bemerkt:

  1. Unnötige Verwendung von OUTER APPLY. CROSS APPLYSie erhalten die gleichen Ergebnisse und vermeiden bei dieser Abfrage eine Reihe unnötiger Verknüpfungen. Das kann ein bisschen Zeit sparen. Es hängt hauptsächlich davon ab, ob die vollständige Abfrage parallel verläuft. Ich weiß nichts über Ihre Daten oder Abfragen, also teste ich nur mit MAXDOP 1. In diesem Fall bin ich besser dran CROSS APPLY.

  2. Es gibt viele CHARINDEXAufrufe, wenn Sie nur nach einem Zeichen anhand einer kleinen Liste übereinstimmender Werte suchen. Sie können die ASCII()Funktion und ein wenig Mathematik verwenden, um alle Zeichenfolgenvergleiche zu vermeiden.

Hier ist eine andere Art, die Funktion zu schreiben:

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

Auf meinem Computer ist die neue Funktion deutlich schneller:

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

Es gibt wahrscheinlich auch einige zusätzliche Optimierungen, aber mein Bauch sagt, dass sie nicht viel ausmachen werden. Aufgrund dessen, was Ihr Code tut, kann ich nicht sehen, wie Sie weitere Verbesserungen sehen würden, wenn Sie Ihre Funktion auf eine andere Weise aufrufen würden. Es ist nur eine Reihe von String-Operationen. Das 20-malige Aufrufen der Funktion pro Zeile ist langsamer als nur einmal, aber die Definition wird bereits eingefügt.

Joe Obbish
quelle
Danke dafür. Wollen Sie mit "Definition wird bereits inline" sagen, dass sich die Ausführung der TVF in mehreren Spalten wie eine Inline-Funktion verhält?
Mazhar
Ich werde Ihre Funktion anhand meiner Daten testen.
Mazhar
2

Versuchen Sie Folgendes zu verwenden

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

stattdessen

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

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

Eine Variante mit Verwendung einer Hilfstabelle

-- 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','-')

Eine Testabfrage

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

Als Variante können Sie auch versuchen, eine temporäre Hilfstabelle #LastCharLinkoder eine Variablentabelle zu verwenden @LastCharLink(diese kann jedoch langsamer sein als eine reale oder temporäre Tabelle).

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',''),
...

Und benutze es als

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

oder

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

Dann können Sie auch eine einfache Inline-Funktion erstellen und alle Konvertierungen einfügen

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

Und dann nutzen Sie diese Funktion als

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
Sergey Menshov
quelle
Ich habe meine Antwort aktualisiert. Versuchen Sie, eine Hilfstabelle zu verwenden, um das zu tun, was Sie wollen. Ich denke, diese Variante wird schneller sein.
Ich habe meine Antwort noch einmal aktualisiert. Jetzt verwendet es Prefixanstelle von Divider.
2

Alternativ können Sie eine permanente Tabelle erstellen. Dies ist eine einmalige Erstellung.

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

Dann 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

Aus @ Joe Beispiel,

- Es dauert 30 s

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

Wenn es möglich ist, kann der Betrag auch auf UI-Ebene formatiert werden. Dies ist die beste Option. Andernfalls können Sie auch Ihre ursprüngliche Abfrage freigeben. ODER wenn möglich, formatieren Sie den formatierten Wert auch in der Tabelle.

KumarHarsh
quelle