2009-03-26 10 views
1

J'ai créé une fonction définie par l'utilisateur pour obtenir des performances avec les requêtes contenant 'WHERE col IN (...)', comme ce cas:fonction définie par l'utilisateur remplaçant WHERE IN col (...)

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

Le les requêtes sont générées à partir d'une application web et sont dans certains cas beaucoup plus complexes. La définition de la fonction ressemble à ceci:

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 

La fonction est destinée à être utilisée comme celui-ci (ou comme INNER JOIN):

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

Avez-ce que quelqu'un a des idears de optimiztion de ma fonction ou autre façons d'améliorer les performances dans mon cas? Y at-il des inconvénients que j'ai manqués? J'utilise le framework MS SQL Server 2005 Std et .NET 2.0.

Répondre

1

La solution CLR ne m'a pas donné une bonne performance donc j'utiliserai une requête récursive. Voici donc la définition du SP je vais utiliser (la plupart du temps en fonction des exemples de Erland):

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

Je ne suis pas sûr de l'augmentation des performances, mais je l'utiliserais comme une jointure interne et échapperais à l'instruction de sélection interne.

1

L'utilisation d'un fichier UDF dans une clause WHERE ou (pire) une sous-requête pose problème. L'optimiseur a parfois raison, mais il se trompe souvent et évalue la fonction une fois pour chaque ligne de votre requête, ce que vous ne voulez pas. Si vos paramètres sont statiques (ils semblent l'être) et que vous pouvez lancer un batch multi-états, je chargerais les résultats de votre UDF dans une variable de table, puis utiliser une jointure avec la variable table pour faire votre filtrage. Cela devrait fonctionner de manière plus fiable.

1

cette boucle va tuer les performances!

créer une table comme ceci:

CREATE TABLE Numbers 
(
    Number int not null primary key 
) 

qui a des lignes contenant des valeurs de 1 à 8000 environ et utiliser cette fonction:

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

J'ai d'autres versions qui ne renvoient pas vide ou null lignes, celles qui renvoient juste l'élément et non le numéro de ligne, etc. Regardez dans le commentaire d'en-tête pour voir comment l'utiliser dans le cadre d'un JOIN, qui est beaucoup plus rapide que dans une clause where.

+0

+1 pour une bonne suggestion, mais je vais utiliser une version qui gère BIGINT aussi bien et la table de nombre augmentera beaucoup! –

+0

La table Nombre doit seulement être aussi grande que la plus longue chaîne. Si vous prévoyez d'avoir plus de 8000 caractères dans la chaîne fractionnée, ajoutez simplement plus de lignes.Le nombre d'int dans la table de nombre peut aller jusqu'à 2 147 483 647 et je doute fort que vous ayez une chaîne aussi longue. –

0

Merci pour la contribution, je dois admettre que j'ai fait quelques mauvaises recherches avant de commencer mon travail. J'ai trouvé que Erland Sommarskog a écrit beaucoup de ce problème sur sa page Web, après vos responeses et après avoir lu sa page, j'ai décidé que je vais essayer de faire un CLR pour résoudre ce problème.

J'ai essayé une requête récursive, cela a abouti à de bonnes performances, mais je vais essayer la fonction CLR de toute façon.

+0

à partir de la page Web ci-dessus sur les performances du CLR: ... lorsqu'ils ont effectué des tests multi-processus, la méthode CLR s'est considérablement améliorée par rapport aux autres méthodes. En fait, il a tellement évolué, qu'ils ont été abandonnés au profit d'autres méthodes. –

+0

Je n'ai pas essayé le split CLR, mais je parierais que ce serait beaucoup plus lent que la fonction avec laquelle j'ai répondu. –

Questions connexes