web-dev-qa-db-fra.com

Pourquoi les serveurs liés ont-ils une limitation de 10 branches dans une expression CASE?

Pourquoi cette expression CASE:

SELECT CASE column 
        WHEN 'a' THEN '1' 
        WHEN 'b' THEN '2' 
        ... c -> i
        WHEN 'j' THEN '10' 
        WHEN 'k' THEN '11'  
    END [col] 
FROM LinkedServer.database.dbo.table

Produire ce résultat?

Message d'erreur: Msg 8180, niveau 16, état 1, ligne 1
La ou les déclarations n'ont pas pu être préparées.
Msg 125, niveau 15, état 4, ligne 1
Les expressions de cas ne peuvent être imbriquées qu'au niveau 10.

Clairement, il n'y a pas d'expression CASE imbriquée ici, bien qu'il y ait plus de 10 "branches".

Encore une bizarrerie. Cette fonction de valeur de table en ligne produit la même erreur:

ALTER FUNCTION [dbo].[fn_MyFunction]
(   
     @var varchar(20)
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT CASE column 
            WHEN 'a' THEN '1' 
            WHEN 'b' THEN '2' 
            ... c -> i
            WHEN 'j' THEN '10' 
            WHEN 'k' THEN '11'  
        END [col] 
    FROM LinkedServer.database.dbo.table
)

Mais un TVF multi-déclarations similaire fonctionne très bien:

ALTER FUNCTION [dbo].[fn_MyFunction]
(   
    @var varchar(20)
)
RETURNS @result TABLE 
(
    value varchar(max)
)
AS
BEGIN
    INSERT INTO @result
    SELECT CASE column 
            WHEN 'a' THEN '1' 
            WHEN 'b' THEN '2' 
            ... c -> i
            WHEN 'j' THEN '10' 
            WHEN 'k' THEN '11'  
        END [col] 
    FROM LinkedServer.database.dbo.table

RETURN;
END
19
Andrey

De toute évidence, il n'y a pas d'expression CASE imbriquée ici.

Pas dans le texte de la requête, non. Mais l'analyseur étend toujours les expressions CASE à la forme imbriquée:

SELECT CASE SUBSTRING(p.Name, 1, 1)
        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 'j' THEN '10' 
        WHEN 'k' THEN '11'  
    END
FROM AdventureWorks2012.Production.Product AS p

Local query plan

Cette requête est locale (pas de serveur lié) et le calcul scalaire définit l'expression suivante:

Nested CASE expression

C'est très bien lorsqu'il est exécuté localement, car parser ne voit pas une instruction CASE imbriquée sur 10 niveaux de profondeur (bien qu'elle en transmette une aux étapes ultérieures de la compilation de la requête locale).

Cependant, avec un serveur lié, le texte généré peut être envoyé au serveur distant pour compilation. Si tel est le cas, le analyseur distant voit une instruction CASE imbriquée de plus de 10 niveaux de profondeur et vous obtenez l'erreur 8180.

ne autre bizarrerie. Cette fonction de table en ligne produit la même erreur

La fonction en ligne est développée sur place dans le texte de requête d'origine, il n'est donc pas surprenant que les mêmes résultats d'erreur avec le serveur lié.

Mais un TVF multi-déclarations similaire fonctionne très bien

Similaire, mais pas le même. Le msTVF implique une conversion implicite en varchar(max), ce qui empêche l'envoi de l'expression CASE au serveur distant. Étant donné que le CASE est évalué localement, un analyseur ne voit jamais un CASE sur-imbriqué et il n'y a pas d'erreur. Si vous changez la définition de table de varchar(max) en type implicite du résultat CASE - varchar(2) - l'expression est distante avec msTVF et vous obtiendrez une erreur.

Finalement, l'erreur se produit lorsqu'un CASE sur-imbriqué est évalué par le serveur distant. Si le CASE n'est pas évalué dans l'itérateur de requête distante, aucune erreur ne se produit. Par exemple, ce qui suit inclut un CONVERT qui n'est pas distant, donc aucune erreur ne se produit même si un serveur lié est utilisé:

SELECT CASE CONVERT(varchar(max), SUBSTRING(p.Name, 1, 1))
        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 'j' THEN '10' 
        WHEN 'k' THEN '11'  
    END
FROM SQL2K8R2.AdventureWorks.Production.Product AS p

CASE not remoted

24
Paul White 9

Mon intuition est que la requête est réécrite quelque part en cours de route pour avoir une structure CASE légèrement différente, par exemple.

CASE WHEN column = 'a' THEN '1' ELSE CASE WHEN column = 'b' THEN '2' ELSE ...

Je crois que c'est un bogue dans le fournisseur de serveur lié que vous utilisez (en fait peut-être tous - je l'ai vu contre plusieurs). Je crois également que vous ne devriez pas retenir votre souffle en attendant un correctif, que ce soit dans la fonctionnalité ou dans le message d'erreur déroutant expliquant le comportement - cela a été signalé depuis longtemps, implique des serveurs liés (qui n'ont pas eu beaucoup d'amour depuis SQL Server 2000), et affecte beaucoup moins de personnes que ce message d'erreur déroutant , qui n'a pas encore été corrigé après la même longévité.

Comme souligne Paul , SQL Server étend votre expression CASE à la variété imbriquée et le serveur lié ne l'aime pas. Le message d'erreur est déroutant, mais uniquement parce que la conversion sous-jacente de l'expression n'est pas immédiatement visible (ni intuitive en aucune façon).

Une solution de contournement (autre que la modification de fonction que vous avez ajoutée à votre question) consisterait à créer une vue ou une procédure stockée sur le serveur lié et à y faire référence au lieu de passer la requête complète via le fournisseur de serveur lié.

Un autre (en supposant que votre requête est vraiment simpliste et que vous voulez juste le coefficient numérique des lettres a-z) est d'avoir:

SELECT [col] = RTRIM(ASCII([column])-96)
FROM LinkedServer.database.dbo.table;

Si vous avez absolument besoin que cela fonctionne tel quel, je vous suggère de contacter directement le support et d'ouvrir un dossier, bien que je ne puisse pas garantir les résultats - ils peuvent simplement vous fournir des solutions de contournement auxquelles vous avez déjà accès sur cette page.

6
Aaron Bertrand

vous pouvez contourner cela en

SELECT COALESCE(
CASE SUBSTRING(p.Name, 1, 1)
    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' 
    ELSE NULL
END,
CASE SUBSTRING(p.Name, 1, 1)
    WHEN 'j' THEN '10' 
    WHEN 'k' THEN '11'  
END)
FROM SQL2K8R2.AdventureWorks.Production.Product AS p
5
Nik

Une autre solution à ce problème consiste à utiliser une logique basée sur un ensemble, en remplaçant l'expression CASE par une jointure gauche (ou une application externe) à une table de référence (ref dans le code ci-dessous), qui peut être soit permanente, temporaire ou une table dérivée/CTE. Si cela est nécessaire dans plusieurs requêtes et procédures, je préférerais l'avoir comme table permanente:

SELECT ref.result_column AS [col] 
FROM LinkedServer.database.dbo.table AS t
  LEFT JOIN
    ( VALUES ('a',  '1'),
             ('b',  '2'), 
             ('c',  '3'),
             ---
             ('j', '10'),
             ('k', '11')
    ) AS ref (check_col, result_column) 
    ON ref.check_col = t.column ;
2
ypercubeᵀᴹ