Cara paling efisien untuk memanggil Fungsi Table-Valued yang sama pada beberapa kolom dalam Kueri

8

Saya mencoba untuk menyetel kueri tempat fungsi bernilai tabel (TVF) yang sama dipanggil pada 20 kolom.

Hal pertama yang saya lakukan adalah mengubah fungsi skalar menjadi fungsi bernilai tabel inline.

Apakah menggunakan CROSS APPLYcara berkinerja terbaik untuk menjalankan fungsi yang sama pada banyak kolom dalam kueri?

Contoh sederhana:

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

Apakah ada alternatif yang lebih baik?

Fungsi yang sama dapat dipanggil dalam beberapa kueri terhadap jumlah kolom X.

Inilah fungsinya:

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

Inilah versi fungsi skalar yang saya warisi, jika ada yang tertarik:

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

Sampel data uji:

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
sumber

Jawaban:

8

PERTAMA: harus disebutkan bahwa metode tercepat untuk mendapatkan hasil yang diinginkan adalah dengan melakukan hal berikut:

  1. Migrasikan data ke kolom baru atau bahkan tabel baru:
    1. Pendekatan kolom baru:
      1. Tambahkan kolom baru {name}_newke tabel dengan DECIMAL(18, 3)tipe data
      2. Lakukan migrasi satu kali data dari VARCHARkolom lama ke DECIMALkolom
      3. ganti nama kolom lama menjadi {name}_old
      4. ganti nama kolom baru menjadi adil {name}
    2. Pendekatan tabel baru:
      1. Buat tabel baru dengan {table_name}_newmenggunakan DECIMAL(18, 3)tipe data
      2. Lakukan migrasi satu kali data dari tabel saat ini ke tabel baru DECIMALberbasis.
      3. ganti nama tabel lama menjadi _old
      4. hapus _newdari tabel baru
  2. Perbarui aplikasi, dll untuk tidak pernah memasukkan data yang disandikan dengan cara ini
  3. setelah satu siklus rilis, jika tidak ada masalah, jatuhkan kolom atau tabel lama
  4. jatuhkan TVFs dan UDF
  5. Jangan pernah membicarakan ini lagi!

YANG TELAH DIBAYARKAN: Anda dapat menyingkirkan banyak kode itu karena duplikasi ini sebagian besar tidak perlu. Juga, setidaknya ada dua bug yang menyebabkan output terkadang tidak benar, atau kadang-kadang membuat kesalahan. Dan bug-bug itu disalin ke dalam kode Joe karena menghasilkan hasil yang sama (termasuk kesalahan) seperti kode OP. Sebagai contoh:

  • Nilai-nilai ini menghasilkan hasil yang benar:

    00062929x
    00021577E
    00000509H
  • Nilai-nilai ini menghasilkan hasil yang salah:

    00002020Q
    00016723L
    00009431O
    00017221R
  • Nilai ini menghasilkan kesalahan:

    00062145}
    anything ending with "}"

Membandingkan semua 3 versi dengan 448.740 baris menggunakan SET STATISTICS TIME ON;, mereka semua berlari di lebih dari 5000 ms dari waktu yang telah berlalu. Tetapi untuk waktu CPU, hasilnya adalah:

  • TVF OP: 7031 ms
  • TVF Joe: 3734 ms
  • TVF Solomon: 1407 ms

SETUP: DATA

Berikut ini membuat tabel dan mengisinya. Ini harus membuat kumpulan data yang sama di semua sistem yang menjalankan SQL Server 2017 karena mereka akan memiliki baris yang sama di spt_values. Ini membantu memberikan dasar perbandingan di antara orang lain yang melakukan pengujian pada sistem mereka karena data yang dihasilkan secara acak akan menjadi faktor perbedaan waktu antar sistem, atau bahkan antara pengujian pada sistem yang sama jika data sampel dibuat ulang. Saya mulai dengan tabel 3 kolom yang sama seperti yang dilakukan Joe, tetapi menggunakan nilai sampel dari pertanyaan sebagai templat untuk menghasilkan berbagai nilai numerik yang ditambahkan dengan masing-masing opsi karakter trailing yang mungkin (termasuk tidak ada karakter trailing). Ini juga mengapa saya memaksa Collation pada kolom: Saya tidak ingin fakta bahwa saya menggunakan Instance-biner Collation untuk secara tidak langsung meniadakan efek menggunakanCOLLATE kata kunci untuk memaksa Collation berbeda di TVF).

Satu-satunya perbedaan adalah dalam urutan baris dalam tabel.

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

Tolong dicatat:

  1. Saya menggunakan _BIN2Kolasi biner (yaitu ) yang lebih cepat daripada Kolasi case-sensitive karena tidak perlu memperhitungkan aturan linguistik.
  2. Satu-satunya hal yang benar-benar penting adalah lokasi (yaitu "indeks") dari karakter paling kanan dalam daftar karakter alfa ditambah dua kurung keriting. Segala sesuatu yang dilakukan secara operasional berasal dari posisi itu lebih dari nilai karakter itu sendiri.
  3. Saya menggunakan parameter input dan mengembalikan tipe data seperti yang ditunjukkan dalam UDF asli yang ditulis ulang oleh OP Kecuali jika ada alasan bagus untuk beralih dari VARCHAR(50)ke VARCHAR(60), dan dari NUMERIC (18,3)ke NUMERIC (18,2)(alasan yang baik adalah "mereka salah"), maka saya akan tetap menggunakan dengan tanda tangan / jenis asli.
  4. Saya menambahkan titik periode / desimal ke ujung 3 numerik literal / konstanta: 100., -1., dan 1.. Ini bukan dalam versi asli TVF saya ini (dalam sejarah jawaban ini) tapi saya perhatikan beberapa CONVERT_IMPLICITpanggilan dalam rencana eksekusi XML (karena 100ini adalah INTtetapi operasinya perlu NUMERIC/ DECIMAL) jadi saya hanya mengurusnya sebelumnya .
  5. Saya membuat karakter string menggunakan CHAR()fungsi daripada meneruskan versi string dari angka (misalnya '2') ke dalam CONVERTfungsi (yang pada awalnya saya lakukan, lagi dalam sejarah). Ini tampaknya sedikit lebih cepat. Hanya beberapa milidetik, tapi tetap saja.

UJI

Harap perhatikan bahwa saya harus memfilter baris yang diakhiri }karena hal itu menyebabkan TVF OP dan Joe salah. Sementara kode saya menangani dengan }benar, saya ingin konsisten dengan baris apa yang sedang diuji di 3 versi. Inilah sebabnya mengapa jumlah baris yang dihasilkan oleh kueri pengaturan sedikit lebih tinggi dari jumlah yang saya catat di atas hasil pengujian untuk berapa banyak baris yang sedang diuji.

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

Waktu CPU hanya sedikit lebih rendah saat membatalkan komentar --@Dummy =, dan peringkat di antara 3 TVF sama. Namun yang cukup menarik, ketika menghapus komentar variabel, peringkat berubah sedikit:

  • TVF Joe: 3295 ms
  • TVF OP: 2240 ms
  • TVF Solomon: 1203 ms

Tidak yakin mengapa kode OP akan tampil jauh lebih baik dalam skenario ini (sedangkan kode saya dan Joe hanya meningkat sedikit), tetapi tampaknya konsisten di banyak tes. Dan tidak, saya tidak melihat perbedaan rencana eksekusi karena saya tidak punya waktu untuk menyelidiki itu.

BAHKAN LEBIH CEPAT

Saya telah menyelesaikan pengujian pendekatan alternatif dan itu memang memberikan sedikit perbaikan tetapi pasti untuk apa yang ditunjukkan di atas. Pendekatan baru menggunakan SQLCLR dan tampaknya skala lebih baik. Saya menemukan bahwa ketika menambahkan di kolom kedua ke kueri, pendekatan T-SQL berlipat ganda dalam waktu. Tetapi, ketika menambahkan kolom tambahan menggunakan SQLCLR Scalar UDF, waktu meningkat, tetapi tidak dengan jumlah yang sama dengan waktu kolom tunggal. Mungkin ada beberapa overhead awal dalam memanggil metode SQLCLR (tidak terkait dengan overhead pemuatan awal App Domain dan Majelis ke dalam App Domain) karena waktunya adalah (waktu berlalu, bukan waktu CPU):

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

Jadi ada kemungkinan bahwa waktu (membuang ke variabel, tidak mengembalikan set hasil) memiliki overhead 200 ms - 250 ms dan kemudian 750 ms - 800 ms per waktu contoh. Timing CPU adalah: 950 ms, 1750 ms, dan 2400 ms untuk 1, 2, dan 3 instance dari UDF, masing-masing.

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

Saya awalnya digunakan SqlDecimalsebagai tipe pengembalian, tetapi ada penalti kinerja untuk menggunakannya sebagai lawan dari SqlDouble/ FLOAT. Kadang-kadang FLOAT memiliki masalah (karena itu menjadi tipe yang tidak tepat), tetapi saya memverifikasi terhadap T-SQL TVF melalui permintaan berikut dan tidak ada perbedaan yang terdeteksi:

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

UJI

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
sumber
Terima kasih untuk ini. Saya akan menguji fungsi Anda terhadap data saya. Berharap untuk melihat perubahan Anda untuk membuatnya lebih cepat dan menguji data.
Mazhar
1
@Mazhar Terima kasih telah menerima :-). Namun, saya telah menyelesaikan pengujian saya pada pendekatan alternatif dan menemukan bahwa itu sedikit lebih cepat daripada apa yang sudah saya miliki di sini. Ini menggunakan SQLCLR tetapi skala lebih baik. Ini juga kembali menjadi skalar UDF sehingga sedikit lebih mudah untuk dikerjakan (yaitu tidak membutuhkan CROSS APPLYs).
Solomon Rutzky
" Mungkin ada beberapa overhead awal dalam memanggil metode SQLCLR (tidak terkait dengan overhead dari loading awal App Domain dan Majelis ke dalam App Domain) " - Saya akan menyarankan bahwa overhead tersebut mungkin kompilasi JIT, karena hanya ditemui pada jalankan pertama. Tapi saya memprofilkan kode Anda dalam aplikasi konsol C #, dan hanya menghasilkan 10's ms dari kompilasi JIT. Metode statis secara khusus hanya membutuhkan 0,3 ms untuk menjadi JIT. Tapi saya tidak tahu apa-apa tentang SQLCLR, jadi mungkin ada lebih banyak kode yang terlibat daripada yang saya ketahui.
Josh Darnell
1
@ jadarnel27 Terima kasih telah membantu menyelidiki. Saya pikir itu mungkin pemeriksaan izin sesuatu. Sesuatu yang berkaitan dengan menghasilkan / memvalidasi rencana kueri.
Solomon Rutzky
4

Saya akan mulai dengan melemparkan beberapa data uji ke dalam tabel. Saya tidak tahu seperti apa data asli Anda sehingga saya hanya menggunakan integer berurutan:

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;

Memilih semua baris dengan set hasil dimatikan menyediakan garis dasar:

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

Jika permintaan serupa dengan pemanggilan fungsi membutuhkan lebih banyak waktu maka kami memiliki taksiran kasar mengenai overhead fungsi. Inilah yang saya dapatkan dengan memanggil TVF Anda apa adanya:

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

Jadi fungsinya membutuhkan sekitar 40 detik waktu CPU untuk 6,5 juta baris. Lipat gandakan dengan 20 dan waktu CPU 800 detik. Saya perhatikan dua hal dalam kode fungsi Anda:

  1. Penggunaan yang tidak perlu OUTER APPLY. CROSS APPLYakan memberi Anda hasil yang sama, dan untuk permintaan ini, ia akan menghindari banyak gabungan yang tidak perlu. Itu bisa menghemat sedikit waktu. Sebagian besar tergantung pada apakah kueri lengkapnya berjalan paralel. Saya tidak tahu apa-apa tentang data atau permintaan Anda, jadi saya hanya mengujinya MAXDOP 1. Dalam hal ini saya lebih baik dengan CROSS APPLY.

  2. Ada banyak CHARINDEXpanggilan saat Anda hanya mencari satu karakter dengan sejumlah kecil nilai yang cocok. Anda dapat menggunakan ASCII()fungsi dan sedikit matematika untuk menghindari semua perbandingan string.

Berikut cara berbeda untuk menulis fungsi:

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

Di mesin saya, fungsi baru secara signifikan lebih cepat:

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

Mungkin ada beberapa optimasi tambahan juga tersedia, tetapi nyali saya mengatakan mereka tidak akan banyak berarti. Berdasarkan apa yang dilakukan kode Anda, saya tidak dapat melihat bagaimana Anda akan melihat peningkatan lebih lanjut dengan cara memanggil fungsi Anda dengan cara yang berbeda. Ini hanya sekelompok operasi string. Memanggil fungsi 20 kali per baris akan lebih lambat dari sekali saja, tetapi definisi tersebut sudah dimasukkan.

Joe Obbish
sumber
Terima kasih untuk ini. Apakah Anda mengatakan dengan "definisi sudah diuraikan" bahwa pelaksanaan TVF pada banyak kolom akan berperilaku seperti Fungsi Inline?
Mazhar
Saya akan menguji fungsi Anda terhadap data saya.
Mazhar
2

Coba gunakan yang berikut ini

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

sebagai gantinya

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

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

Satu varian dengan menggunakan tabel tambahan

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

Permintaan uji

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

Sebagai varian Anda juga dapat mencoba menggunakan tabel tambahan sementara #LastCharLinkatau tabel variabel @LastCharLink(tetapi bisa lebih lambat dari tabel nyata atau sementara)

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

Dan gunakan sebagai

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

atau

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

Kemudian Anda juga dapat membuat fungsi inline sederhana dan memasukkan semua konversi ke dalamnya

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

Dan kemudian gunakan fungsi ini sebagai

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
sumber
Saya telah memperbarui jawaban saya. Cobalah menggunakan tabel tambahan untuk melakukan apa yang Anda inginkan. Saya pikir varian ini akan lebih cepat.
Saya telah memperbarui jawaban saya sekali lagi. Sekarang ia menggunakan Prefixalih-alih Divider.
2

Atau Anda dapat membuat satu tabel permanen. Ini adalah satu kali pembuatan.

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

Lalu 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

Dari contoh @Joe,

- Butuh 30 detik

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

Jika memungkinkan, Jumlah juga dapat diformat di tingkat UI. Ini pilihan terbaik. Kalau tidak, Anda juga dapat membagikan permintaan asli Anda. ATAU jika memungkinkan, simpan juga nilai yang diformat dalam tabel.

KumarHarsh
sumber