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

19

Pourquoi cette CASEexpression:

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
déclaration (s) n'a pas pu être préparé.
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' CASEexpression 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
Andrey
la source

Réponses:

24

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

Pas dans le texte de la requête, non. Mais l'analyseur étend toujours les CASEexpressions à 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

Plan de requête local

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

Expression CASE imbriquée

C'est très bien lorsqu'il est exécuté localement, car l' analyseur ne voit pas une CASEinstruction imbriquée sur 10 niveaux (bien qu'il en transmette un 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, l' analyseur distant voit une CASEinstruction imbriquée de plus de 10 niveaux de profondeur et vous obtenez l'erreur 8180.

Encore une bizarrerie. Cette fonction table en ligne génère la même erreur

La fonction en ligne est développée en 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 vers varchar(max), ce qui se produit pour empêcher l' CASEexpression d'être envoyée au serveur distant. Parce que le CASEest évalué localement, un analyseur ne voit jamais un sur-imbriqué CASEet il n'y a pas d'erreur. Si vous changez la définition de table de varchar(max)en type implicite du CASErésultat - varchar(2)- l'expression est distante avec le msTVF et vous obtiendrez une erreur.

En fin de compte, l'erreur se produit lorsqu'un sur-imbriqué CASEest évalué par le serveur distant. Si le CASEn'est pas évalué dans l'itérateur de requête distante, aucune erreur ne se produit. Par exemple, ce qui suit inclut un CONVERTqui 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

CAS non déporté

Paul White dit GoFundMonica
la source
6

Mon intuition est que la requête est réécrite quelque part en cours de route pour avoir une CASEstructure 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 signalé contre plusieurs). Je crois également que vous ne devriez pas retenir votre souffle en attendant une correction, 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 le souligne Paul , SQL Server étend votre CASEexpression à 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 az) 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.

Aaron Bertrand
la source
5

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
Nik
la source
2

Une autre solution à ce problème consiste à utiliser une logique basée sur un ensemble, en remplaçant l' CASEexpression par une jointure gauche (ou une application externe) à une table de référence ( refdans le code ci-dessous), qui peut être permanente, temporaire ou une table / CTE dérivée. 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 ;
ypercubeᵀᴹ
la source
-4

une façon de contourner ce problème consiste à inclure le test dans la whenclause à savoir

case
  when SUBSTRING(p.Name, 1, 1) = 'a' THEN '1'
...
user98586
la source
En fait non. Les deux SELECT CASE v.V WHEN 'a' THEN 1 WHEN 'b' THEN 2 END FROM (VALUES ('a'), ('b')) AS v (V);et se SELECT CASE WHEN v.V = 'a' THEN 1 WHEN v.V = 'b' THEN 2 END FROM (VALUES ('a'), ('b')) AS v (V);traduisent exactement par le même plan d'exécution (n'hésitez pas à le vérifier par vous-même), où l'expression CASE est redéfinie comme CASE WHEN [Union1002]='a' THEN (1) ELSE CASE WHEN [Union1002]='b' THEN (2) ELSE NULL END END- avec l'imbrication, comme vous pouvez le voir.
Andriy M