Utilisation de EXCEPT dans une expression de table commune récursive


33

Pourquoi la requête suivante renvoie-t-elle un nombre infini de lignes? Je me serais attendu à ce que la EXCEPTclause mette fin à la récursivité.

with cte as (
    select *
    from (
        values(1),(2),(3),(4),(5)
    ) v (a)
)
,r as (
    select a
    from cte
    where a in (1,2,3)
    union all
    select a
    from (
        select a
        from cte
        except
        select a
        from r
    ) x
)
select a
from r

Je suis tombé sur cela en essayant de répondre à une question sur Stack Overflow.

Réponses:


26

Voir la réponse de Martin Smith pour des informations sur l'état actuel d' EXCEPTun CTE récursif.

Pour expliquer ce que vous voyiez et pourquoi:

J'utilise une variable de table ici, pour faire la distinction entre les valeurs d'ancrage et l'item récursif plus clair (cela ne change pas la sémantique).

DECLARE @V TABLE (a INTEGER NOT NULL)
INSERT  @V (a) VALUES (1),(2)
;
WITH rCTE AS 
(
    -- Anchor
    SELECT
        v.a
    FROM @V AS v

    UNION ALL

    -- Recursive
    SELECT
        x.a
    FROM
    (
        SELECT
            v2.a
        FROM @V AS v2

        EXCEPT

        SELECT
            r.a
        FROM rCTE AS r
    ) AS x
)
SELECT
    r2.a
FROM rCTE AS r2
OPTION (MAXRECURSION 0)

Le plan de requête est:

Plan de CTE récursif

L'exécution commence à la racine du plan (SELECT) et le contrôle transmet l'arborescence à la spool d'indexation, à la concaténation, puis à l'analyse de table de niveau supérieur.

La première ligne de l'analyse passe par l'arborescence et est (a) stockée dans la file d'attente de pile et (b) est renvoyée au client. La première ligne n'est pas définie, mais supposons qu'il s'agisse de la ligne avec la valeur {1}, par souci d'argument. La première ligne à apparaître est donc {1}.

Le contrôle repasse de nouveau à l'analyse de la table (l'opérateur de concaténation utilise toutes les lignes de son entrée la plus externe avant d'ouvrir la suivante). L'analyse émet la deuxième ligne (valeur {2}), qui renvoie à nouveau l'arborescence à stocker dans la pile et à envoyer au client. Le client a maintenant reçu la séquence {1}, {2}.

En adoptant une convention où le haut de la pile LIFO est à gauche, la pile contient maintenant {2, 1}. Lorsque le contrôle passe à nouveau au contrôle de table, il ne rapporte plus de lignes et le contrôle revient à l'opérateur de concaténation, qui ouvre sa deuxième entrée (il a besoin d'une ligne pour passer au spool de la pile), et le contrôle passe à la jointure interne. pour la première fois.

La jointure interne appelle le spool de table sur son entrée externe, qui lit la ligne supérieure de la pile {2} et la supprime de la table de travail. La pile contient maintenant {1}.

Ayant reçu une ligne sur son entrée externe, le joint interne passe le contrôle de son entrée interne au joint anti-semi-gauche (LASJ). Cela demande une ligne à partir de son entrée externe, en passant le contrôle au tri. Sort est un itérateur bloquant, il lit donc toutes les lignes de la variable de table et les trie par ordre croissant (comme cela se produit).

La première ligne émise par le tri est donc la valeur {1}. Le côté interne de LASJ renvoie la valeur actuelle du membre récursif (la valeur qui vient de sortir de la pile), qui est {2}. Les valeurs au niveau du LASJ sont {1} et {2}, de sorte que {1} est émis, car les valeurs ne correspondent pas.

Cette ligne {1} relie l’arborescence du plan de requête à la bobine d’index (pile) où elle est ajoutée à la pile, qui contient maintenant {1, 1}, puis émise vers le client. Le client a maintenant reçu la séquence {1}, {2}, {1}.

Le contrôle retourne maintenant à la concaténation, à l'intérieur (il a renvoyé une ligne la dernière fois, peut-être à nouveau), à travers la jointure interne, jusqu'au LASJ. Il relit son entrée interne à nouveau, en obtenant la valeur {2} à partir du tri.

Le membre récursif est toujours {2}. Par conséquent, cette fois, LASJ trouve {2} et {2}, aucune ligne n'étant émise. Ne trouvant plus de lignes sur son entrée interne (le tri est maintenant épuisé), le contrôle est renvoyé à la jointure interne.

La jointure interne lit son entrée externe, ce qui entraîne la suppression de la valeur {1} de la pile {1, 1}, ce qui laisse la pile avec seulement {1}. Le processus se répète maintenant, avec la valeur {2} d'un nouvel appel de Table Scan and Sort, qui passe le test LASJ et est ajouté à la pile, puis passe au client qui a maintenant reçu {1}, {2}. {1}, {2} ... et on y va.

Craig Freedman est l' explication que je préfère de la bobine de pile utilisée dans les plans CTE récursifs.


31

La description BOL des CTE récursifs décrit la sémantique de l'exécution récursive comme suit:

  1. Divisez l'expression CTE en membres d'ancrage et récursifs.
  2. Exécutez le ou les membres d'ancrage créant le premier jeu de résultats d'invocation ou de base (T0).
  3. Exécutez le ou les membres récursifs avec Ti en entrée et Ti + 1 en sortie.
  4. Répétez l'étape 3 jusqu'à ce qu'un jeu vide soit renvoyé.
  5. Renvoie le jeu de résultats. Ceci est une UNION ALL de T0 à Tn.

Notez que ce qui précède est une description logique . L'ordre physique des opérations peut être quelque peu différent, comme illustré ici.

En appliquant cela à votre CTE, je m'attendrais à une boucle infinie avec le motif suivant

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       4 | 5 |   |   |
|         3 |       1 | 2 | 3 |   |
|         4 |       4 | 5 |   |   |
|         5 |       1 | 2 | 3 |   |
+-----------+---------+---+---+---+ 

Car

select a
from cte
where a in (1,2,3)

est l'expression d'ancrage. Ceci retourne clairement 1,2,3commeT0

Ensuite, l'expression récursive s'exécute

select a
from cte
except
select a
from r

Avec 1,2,3comme entrée qui produira un signal de sortie 4,5comme T1puis de brancher ce avant pour le prochain cycle de récurrence sera de retour 1,2,3et ainsi de suite indéfiniment.

Ce n'est pas ce qui se passe réellement cependant. Ce sont les résultats des 5 premières invocations

+-----------+---------+---+---+---+
| Invocation| Results             |
+-----------+---------+---+---+---+
|         1 |       1 | 2 | 3 |   |
|         2 |       1 | 2 | 4 | 5 |
|         3 |       1 | 2 | 3 | 4 |
|         4 |       1 | 2 | 3 | 5 |
|         5 |       1 | 2 | 3 | 4 |
+-----------+---------+---+---+---+

En utilisant OPTION (MAXRECURSION 1)et en augmentant progressivement les incréments, 1on peut voir qu’il entre dans un cycle où chaque niveau successif bascule continuellement entre les sorties 1,2,3,4et 1,2,3,5.

Comme discuté par @Quassnoi dans cet article de blog . Le modèle des résultats observés est comme si chaque appel se déroulait, (1),(2),(3),(4),(5) EXCEPT (X)Xse trouvait la dernière ligne de l'appel précédent.

Edit: Après avoir lu l'excellente réponse de SQL Kiwi, il est clair à la fois pourquoi cela se produit et qu'il ne s'agit pas là de toute l'histoire, car il reste encore beaucoup de choses sur la pile qui ne peuvent jamais être traitées.

Ancre émet 1,2,3au contenu de la pile du client3,2,1

3 piles sautées, contenu de la pile 2,1

Le LASJ revient 1,2,4,5, empile le contenu5,4,2,1,2,1

5 piles sautées, contenu de la pile 4,2,1,2,1

Le LASJ renvoie le 1,2,3,4 contenu de la pile4,3,2,1,5,4,2,1,2,1

4 sauté hors pile, contenu de la pile 3,2,1,5,4,2,1,2,1

Le LASJ renvoie le 1,2,3,5 contenu de la pile5,3,2,1,3,2,1,5,4,2,1,2,1

5 piles sautées, contenu de la pile 3,2,1,3,2,1,5,4,2,1,2,1

Le LASJ renvoie le 1,2,3,4 contenu de la pile 4,3,2,1,3,2,1,3,2,1,5,4,2,1,2,1

Si vous essayez de remplacer le membre récursif par l'expression logiquement équivalente (en l'absence de doublons / NULL)

select a
from (
    select a
    from cte
    where a not in 
    (select a
    from r)
) x

Ceci n'est pas autorisé et génère l'erreur "Les références récursives ne sont pas autorisées dans les sous-requêtes." alors c'est peut-être un oubli qui EXCEPTest même permis dans ce cas.

Ajout: Microsoft a maintenant répondu à mes commentaires sur Connect, comme indiqué ci-dessous.

La supposition de Jack est correcte: cela aurait dû être une erreur de syntaxe; les références récursives ne devraient en effet pas être autorisées dans les EXCEPTclauses. Nous prévoyons de résoudre ce problème dans une prochaine version du service. Entre-temps, je suggérerais d'éviter les références récursives dans les EXCEPT clauses.

En limitant la récursivité, EXCEPTnous respectons le standard ANSI SQL, qui inclut cette restriction depuis l’introduction de la récursivité (en 1999, je crois). Il n'y a pas d'accord général sur ce que la sémantique devrait être pour la récursion EXCEPT(également appelée "négation non stratifiée") dans des langages déclaratifs tels que SQL. En outre, il est notoirement difficile (voire impossible) de mettre en œuvre efficacement une telle sémantique (pour des bases de données de taille raisonnable) dans un système de SGBDR.

Et on dirait que la mise en œuvre éventuelle a été réalisée en 2014 pour les bases de données avec un niveau de compatibilité de 120 ou plus .

Les références récursives dans une clause EXCEPT génèrent une erreur conforme au standard SQL ANSI.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.