Определяемая пользователем функция заменяет WHERE col IN (...) - PullRequest
1 голос
/ 26 марта 2009

Я создал пользовательскую функцию для повышения производительности с запросами, содержащими 'WHERE col IN (...)', как в этом случае:

SELECT myCol1, myCol2
FROM myTable
WHERE myCol3 IN (100, 200, 300, ..., 4900, 5000);

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

CREATE FUNCTION [dbo].[udf_CSVtoIntTable]
(
  @CSV VARCHAR(MAX),
  @Delimiter CHAR(1) = ','
)
RETURNS 
@Result TABLE 
(
    [Value] INT
)
AS
BEGIN

  DECLARE @CurrStartPos SMALLINT;
  SET @CurrStartPos = 1;
  DECLARE @CurrEndPos SMALLINT;
  SET @CurrEndPos = 1;
  DECLARE @TotalLength SMALLINT;

  -- Remove space, tab, linefeed, carrier return
  SET @CSV = REPLACE(@CSV, ' ', '');
  SET @CSV = REPLACE(@CSV, CHAR(9), '');
  SET @CSV = REPLACE(@CSV, CHAR(10), '');
  SET @CSV = REPLACE(@CSV, CHAR(13), '');

  -- Add extra delimiter if needed
  IF NOT RIGHT(@CSV, 1) = @Delimiter
    SET @CSV = @CSV + @Delimiter;

  -- Get total string length 
  SET @TotalLength = LEN(@CSV);

  WHILE @CurrStartPos < @TotalLength
  BEGIN

    SET @CurrEndPos = CHARINDEX(@Delimiter, @CSV, @CurrStartPos);

    INSERT INTO @Result
    VALUES (CAST(SUBSTRING(@CSV, @CurrStartPos, @CurrEndPos - @CurrStartPos) AS INT));

    SET @CurrStartPos = @CurrEndPos + 1;

  END

    RETURN 

END

Функция предназначена для использования следующим образом (или как ВНУТРЕННЕЕ СОЕДИНЕНИЕ):

SELECT myCol1, myCol2
FROM myTable
WHERE myCol3 IN (
    SELECT [Value] 
    FROM dbo.udf_CSVtoIntTable('100, 200, 300, ..., 4900, 5000', ',');

Есть ли у кого-нибудь идеалы по оптимизации моей функции или другие способы повышения производительности в моем случае? Есть ли какие-то недостатки, которые я пропустил?

Я использую MS SQL Server 2005 Std и .NET 2.0 framework.

Ответы [ 5 ]

1 голос
/ 16 апреля 2009

Решение CLR не дало мне хорошей производительности, поэтому я буду использовать рекурсивный запрос. Итак, вот определение SP, которое я буду использовать (в основном на примере Эрланда Соммарскога):

CREATE FUNCTION [dbo].[priudf_CSVtoIntTable]
(
  @CSV VARCHAR(MAX),
  @Delimiter CHAR(1) = ','
)
RETURNS 
@Result TABLE 
(
    [Value] INT
)
AS
BEGIN

  -- Remove space, tab, linefeed, carrier return
  SET @CSV = REPLACE(@CSV, ' ', '');
  SET @CSV = REPLACE(@CSV, CHAR(9), '');
  SET @CSV = REPLACE(@CSV, CHAR(10), '');
  SET @CSV = REPLACE(@CSV, CHAR(13), '');

  WITH csvtbl(start, stop) AS 
  (
    SELECT  start = CONVERT(BIGINT, 1),
            stop = CHARINDEX(@Delimiter, @CSV + @Delimiter)
    UNION ALL
    SELECT  start = stop + 1,
            stop = CHARINDEX(@Delimiter, @CSV + @Delimiter, stop + 1)
    FROM csvtbl
    WHERE stop > 0
  )
  INSERT INTO @Result
  SELECT CAST(SUBSTRING(@CSV, start, CASE WHEN stop > 0 THEN stop - start ELSE 0 END) AS INT) AS [Value]
  FROM   csvtbl
  WHERE  stop > 0
  OPTION (MAXRECURSION 1000)

  RETURN 
END
1 голос
/ 26 марта 2009

Использование UDF в предложении WHERE или (что еще хуже) подзапросе вызывает проблемы. Оптимизатор иногда делает это правильно, но часто ошибается и оценивает функцию один раз для каждой строки в вашем запросе, что вам не нужно.

Если ваши параметры статичны (кажется, что они есть), и вы можете создать многослойный пакет, я бы загрузил результаты вашего UDF в табличную переменную, а затем использовал бы объединение с табличной переменной для выполнения вашей фильтрации. Это должно работать более надежно.

1 голос
/ 26 марта 2009

этот цикл убьет производительность!

создайте таблицу, подобную этой:

CREATE TABLE Numbers
(
    Number  int   not null primary key
)

, который имеет строки, содержащие значения от 1 до 8000 или около того, и использует эту функцию:

CREATE FUNCTION [dbo].[FN_ListAllToNumberTable]
(
     @SplitOn  char(1)       --REQUIRED, the character to split the @List string on
    ,@List     varchar(8000) --REQUIRED, the list to split apart
)
RETURNS
@ParsedList table
(
    RowNumber int
   ,ListValue varchar(500)
)
AS
BEGIN

/*
DESCRIPTION: Takes the given @List string and splits it apart based on the given @SplitOn character.
             A table is returned, one row per split item, with a columns named "RowNumber" and "ListValue".
             This function workes for fixed or variable lenght items.
             Empty and null items will be included in the results set.

PARAMETERS:
    @List      varchar(8000) --REQUIRED, the list to split apart
    @SplitOn   char(1)       --OPTIONAL, the character to split the @List string on, defaults to a comma ","


RETURN VALUES:
  a table, one row per item in the list, with a column name "ListValue"

TEST WITH:
----------
SELECT * FROM dbo.FN_ListAllToNumTable(',','1,12,123,1234,54321,6,A,*,|||,,,,B')

DECLARE @InputList  varchar(200)
SET @InputList='17;184;75;495'
SELECT
    'well formed list',LEFT(@InputList,40) AS InputList,h.Name
    FROM Employee  h
        INNER JOIN dbo.FN_ListAllToNumTable(';',@InputList) dt ON h.EmployeeID=dt.ListValue
    WHERE dt.ListValue IS NOT NULL

SET @InputList='17;;;184;75;495;;;'
SELECT
    'poorly formed list join',LEFT(@InputList,40) AS InputList,h.Name
    FROM Employee  h
        INNER JOIN dbo.FN_ListAllToNumTable(';',@InputList) dt ON h.EmployeeID=dt.ListValue

SELECT
    'poorly formed list',LEFT(@InputList,40) AS InputList, ListValue
    FROM dbo.FN_ListAllToNumTable(';',@InputList)

**/



/*this will return empty rows, and row numbers*/
INSERT INTO @ParsedList
        (RowNumber,ListValue)
    SELECT
        ROW_NUMBER() OVER(ORDER BY number) AS RowNumber
            ,LTRIM(RTRIM(SUBSTRING(ListValue, number+1, CHARINDEX(@SplitOn, ListValue, number+1)-number - 1))) AS ListValue
        FROM (
                 SELECT @SplitOn + @List + @SplitOn AS ListValue
             ) AS InnerQuery
            INNER JOIN Numbers n ON n.Number < LEN(InnerQuery.ListValue)
        WHERE SUBSTRING(ListValue, number, 1) = @SplitOn

RETURN

END /*Function FN_ListAllToNumTable*/

У меня есть другие версии, которые не возвращают пустые или нулевые строки, те, которые возвращают только элемент, а не номер строки и т. Д. Посмотрите в комментарии к заголовку, чтобы увидеть, как использовать это как часть JOIN, что очень быстрее, чем в предложении where.

1 голос
/ 26 марта 2009

Я не уверен в увеличении производительности, но я бы использовал его как внутреннее соединение и ушел от внутреннего оператора select.

0 голосов
/ 27 марта 2009

Спасибо за вклад, я должен признать, что я сделал некоторые плохие исследования, прежде чем я начал свою работу. Я обнаружил, что Erland Sommarskog много писал об этой проблеме на своей веб-странице, после ваших ответов и после прочтения его страницы я решил, что постараюсь создать CLR для решения этой проблемы.

Я пробовал рекурсивный запрос, это привело к хорошей производительности, но я все равно попробую функцию CLR.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...