Самый эффективный способ вызова одной и той же табличной функции для нескольких столбцов в запросе

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

Есть ли лучшие альтернативы?

Одна и та же функция может быть вызвана в нескольких запросах против числа столбцов.

Вот функция:

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
Mazhar
источник

Ответы:

8

ПЕРВЫЙ: следует отметить, что самый быстрый способ получить желаемый результат - это сделать следующее:

  1. Перенос данных в новые столбцы или даже в новую таблицу:
    1. Новый подход столбца:
      1. Добавить новые столбцы {name}_newв таблицу с DECIMAL(18, 3)типом данных
      2. Выполните однократную миграцию данных из старых VARCHARстолбцов в DECIMALстолбцы.
      3. переименовать старые столбцы в {name}_old
      4. переименовать новые столбцы, чтобы быть просто {name}
    2. Новый настольный подход:
      1. Создать новую таблицу, {table_name}_newиспользуя DECIMAL(18, 3)тип данных
      2. Выполните однократную миграцию данных из текущей таблицы в новую DECIMALтаблицу.
      3. переименовать старую таблицу в _old
      4. удалить _newиз новой таблицы
  2. Обновите приложение и т. Д., Чтобы никогда не вставлять данные, закодированные таким образом
  3. после одного цикла выпуска, если нет проблем, удалите старые столбцы или таблицу
  4. сбросить TVF и UDF
  5. Никогда не говори об этом снова!

ЭТО СКАЗАЛ: Вы можете избавиться от большого количества этого кода, поскольку это в значительной степени ненужное дублирование. Кроме того, есть как минимум две ошибки, которые приводят к тому, что выходные данные иногда неверны или иногда выдают ошибку. И эти ошибки были скопированы в код Джо, поскольку он дает те же результаты (включая ошибку), что и код OP. Например:

  • Эти значения дают правильный результат:

    00062929x
    00021577E
    00000509H
    
  • Эти значения дают неверный результат:

    00002020Q
    00016723L
    00009431O
    00017221R
    
  • Это значение выдает ошибку:

    00062145}
    anything ending with "}"
    

Сравнивая все 3 версии с 448 740 строками SET STATISTICS TIME ON;, все они проработали чуть более 5000 мс за прошедшее время. Но для процессорного времени, результаты были:

  • TVF ОП: 7031 мс
  • TVF Джо: 3734 мс
  • TVF Соломона: 1407 мс

НАСТРОЙКА: ДАННЫЕ

Следующее создает таблицу и заполняет ее. Это должно создать один и тот же набор данных во всех системах под управлением SQL Server 2017, поскольку в них будут одинаковые строки spt_values. Это помогает обеспечить основу для сравнения между другими людьми, проводящими тестирование в своей системе, поскольку случайно сгенерированные данные будут учитывать разницу во времени в разных системах или даже между тестами в одной системе, если данные образца регенерированы. Я начал с той же таблицы из 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. Единственное, что действительно имеет значение, - это местоположение (то есть «индекс») самого правого символа в списке буквенных символов плюс две фигурные скобки. Все, что сделано оперативно, происходит от этой позиции больше, чем ценность самого персонажа.
  3. Я использовал входные параметры и возвращаемые значения типа данных , как указан в первоначальном формате 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функцию (что я и делал первоначально в истории). Это кажется немного быстрее. Всего несколько миллисекунд, но все же.

ТЕСТОВОЕ ЗАДАНИЕ

Обратите внимание, что мне пришлось отфильтровывать строки, заканчивающиеся на, }поскольку это приводило к ошибке операторов TVF и Джо. Хотя мой код обрабатывает }правильно, я хотел быть последовательным с тем, какие строки тестировались в трех версиях. Вот почему количество строк, сгенерированных запросом установки, немного больше, чем число, которое я отметил выше по результатам теста для того, сколько строк было проверено.

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 =, и рейтинг среди 3 TVF одинаков. Но что интересно, при раскомментировании переменной рейтинги немного меняются:

  • TVF Джо: 3295 мс
  • TVF ОП: 2240 мс
  • TVF Соломона: 1203 мс

Не уверен, почему код OP будет работать намного лучше в этом сценарии (тогда как код my и Joe только незначительно улучшился), но он кажется непротиворечивым во многих тестах. И нет, я не смотрел на различия в плане выполнения, поскольку у меня нет времени, чтобы исследовать это.

ДАЖЕ БЫСТРО

Я завершил тестирование альтернативного подхода, и он обеспечивает небольшое, но определенное улучшение того, что показано выше. Новый подход использует SQLCLR, и, похоже, он лучше масштабируется. Я обнаружил, что при добавлении во второй столбец запроса подход T-SQL удваивается во времени. Но при добавлении дополнительных столбцов с использованием SQLCLR Scalar UDF время увеличивалось, но не на ту же величину, что и время одного столбца. Возможно, есть некоторые начальные издержки при вызове метода SQLCLR (не связанные с накладными расходами при начальной загрузке домена приложения и сборки в домен приложения), потому что время было (истекшее время, а не время ЦП):

  • 1 столбец: 1018 мс
  • 2 столбца: 1750 - 1800 мс
  • 3 столбца: 2500 - 2600 мс

Таким образом, возможно, что время (дамп в переменную, не возвращая результирующий набор) имеет накладные расходы 200 мс - 250 мс, а затем 750 мс - 800 мс на время экземпляра. Время ЦП составило: 950 мс, 1750 мс и 2400 мс для 1, 2 и 3 экземпляров UDF соответственно.

Код 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;
Соломон Руцкий
источник
Спасибо за это. Я собираюсь проверить вашу функцию по моим данным. Ждем ваших изменений, чтобы сделать их еще быстрее и тестировать данные.
Мажар
1
@ Мажар Спасибо, что приняли :-). Тем не менее, я закончил тестирование на альтернативном подходе и обнаружил, что это немного быстрее, чем у меня уже было здесь. Он использует SQLCLR, но масштабируется лучше. Это также вернулось к тому, чтобы быть скалярным UDF, поэтому с ним немного легче работать (т.е. не нужно CROSS APPLYs).
Соломон Руцкий
« Возможно, есть некоторые начальные издержки при вызове метода SQLCLR (не связанные с накладными расходами при начальной загрузке домена приложения и сборки в домен приложения) » - я собирался предположить, что это может быть компиляция JIT, так как он встречается только при первом запуске. Но я профилировал ваш код в консольном приложении C #, и он потребовал всего 10 мс компиляции JIT. Статический метод определенно занимал всего 3,3 мс для JIT'd. Но я ничего не знаю о 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);

Таким образом, функции требуется около 40 секунд процессорного времени для 6,5 миллионов строк. Умножьте это на 20, и это будет 800 секунд процессорного времени. Я заметил две вещи в вашем коде функции:

  1. Ненужное использование OUTER APPLY. CROSS APPLYдаст вам те же результаты, и для этого запроса он избежит кучу ненужных объединений. Это может сэкономить немного времени. Это в основном зависит от того, идет ли полный запрос параллельно. Я ничего не знаю о ваших данных или запросах, поэтому я просто проверяю MAXDOP 1. В этом случае мне лучше CROSS APPLY.

  2. Есть много CHARINDEXвызовов, когда вы просто ищете один символ по небольшому списку совпадающих значений. Вы можете использовать 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 раз в строке будет медленнее, чем один раз, но определение уже встроено.

Джо Оббиш
источник
Спасибо за это. Вы говорите по определению, что «уже встроено», что выполнение TVF в нескольких столбцах будет вести себя как встроенная функция?
Мажар
Я собираюсь проверить вашу функцию по моим данным.
Мажар
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
...

Один вариант с использованием вспомогательной таблицы

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

В качестве альтернативы вы можете создать одну постоянную таблицу. Это однократное создание.

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

Тогда твф

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

Если это возможно, сумма также может быть отформатирована на уровне пользовательского интерфейса. Это лучший вариант. В противном случае вы также можете поделиться своим оригинальным запросом. ИЛИ, если возможно, сохраняйте форматированное значение в таблице.

KumarHarsh
источник