NEWID () dans la table virtuelle jointe provoque un comportement d'application croisée involontaire

9

Ma requête de travail réelle était une jointure interne, mais cet exemple simple avec jointure croisée semble reproduire presque toujours le problème.

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT NEWID() TEST_ID
) BB ( B )

Avec ma jointure interne, j'avais de nombreuses lignes pour lesquelles j'ai ajouté à chacune un GUID à l'aide de la fonction NEWID (), et pour environ 9 sur 10 de ces lignes, la multiplication avec la table virtuelle à 2 lignes a produit les résultats attendus, seulement 2 copies de le même GUID, tandis que 1 sur 10 produirait des résultats différents. C'était pour le moins inattendu et cela m'a donné beaucoup de mal à essayer de trouver ce bogue dans mon script de génération de données de test.

Si vous regardez les requêtes suivantes en utilisant également les fonctions getdate et sysdatetime non déterministes, vous ne le verrez pas, je ne le vois pas de toute façon - je vois toujours la même valeur datetime dans les deux lignes de résultat final.

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT GETDATE() TEST_ID
) BB ( B )

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT SYSDATETIME() TEST_ID
) BB ( B )

J'utilise actuellement SQL Server 2008 et mon travail pour l'instant consiste à charger mes lignes avec des GUID dans une variable de table avant de terminer mon script de génération de données aléatoires. Une fois que je les ai comme valeurs dans une table par opposition à une table virtuelle, le problème disparaît.

J'ai une solution de contournement, mais je cherche les moyens de contourner ce problème sans tables ni variables de table réelles.

En écrivant ceci, j'ai essayé sans succès ces possibilités: 1) placer le newid () dans une table virtuelle imbriquée:

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT TEST_ID
    FROM (
        SELECT NEWID() TEST_ID
    ) TT
) BB ( B )

2) encapsuler le newid () dans une expression cast telle que:

SELECT CAST(NEWID() AS VARCHAR(100)) TEST_ID

3) inverser l'ordre d'apparition des tables virtuelles dans l'expression de jointure

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS JOIN (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )

4) Utiliser une application croisée non corrélée

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS APPLY (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )

Juste avant de poster finalement cette question, maintenant j'ai essayé avec succès, semble-t-il, une croix corrélée s'applique:

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS APPLY (
    SELECT A
    FROM (
        SELECT 1 UNION ALL
        SELECT 2
    ) TT ( A )
    WHERE BB.B IS NOT NULL
) AA ( A )

Quelqu'un a-t-il une autre solution de contournement plus élégante et plus simple? Je ne veux vraiment pas utiliser d'application croisée ou de corrélation pour une simple multiplication de lignes si je n'ai pas à le faire.

JM Hicks
la source

Réponses:

20

Ce comportement est inhérent à la conception, comme expliqué en détail dans ce rapport de bogue Connect . La réponse Microsoft la plus pertinente est reproduite ci-dessous pour plus de commodité (et au cas où le lien mourrait à un moment donné):

Publié par Microsoft le 07/07/2008 à 09:27

Fermer la boucle . . . J'ai discuté de cette question avec l'équipe de développement. Et finalement, nous avons décidé de ne pas changer le comportement actuel, pour les raisons suivantes:

  1. L'optimiseur ne garantit pas le timing ou le nombre d'exécutions des fonctions scalaires. Il s'agit d'un principe établi de longue date. C'est la «marge de manœuvre» fondamentale qui laisse à l'optimiseur suffisamment de liberté pour obtenir des améliorations significatives dans l'exécution du plan de requête.

  2. Ce "comportement une fois par ligne" n'est pas un nouveau problème, bien qu'il ne soit pas largement discuté. Nous avons commencé à modifier son comportement dans la version du Yukon. Mais il est assez difficile de cerner précisément, dans tous les cas, exactement ce que cela signifie! Par exemple, cela s'applique-t-il aux lignes intermédiaires calculées «en cours» vers le résultat final? - auquel cas cela dépend clairement du plan choisi. Ou s'applique-t-il uniquement aux lignes qui apparaîtront éventuellement dans le résultat final? - il y a une récursion désagréable en cours ici, comme je suis sûr que vous serez d'accord!

  3. Comme je l'ai mentionné plus tôt, nous optons par défaut pour "optimiser les performances" - ce qui est bon pour 99% des cas. Les 1% des cas où cela pourrait changer les résultats sont assez faciles à repérer - des «fonctions» à effets secondaires comme NEWID - et faciles à «corriger» (en conséquence, la performance commerciale). Cette valeur par défaut pour «optimiser à nouveau les performances» est établie de longue date et acceptée. (Oui, ce n'est pas la position choisie par les compilateurs pour les langages de programmation conventionnels, mais qu'il en soit ainsi).

Nos recommandations sont donc les suivantes:

  1. Évitez de vous fier au timing non garanti et à la sémantique du nombre d'exécutions.
  2. Évitez d'utiliser NEWID () au plus profond des expressions de table.
  3. Utilisez OPTION pour forcer un comportement particulier (trading perf)

J'espère que cette explication aide à clarifier nos raisons pour fermer ce bogue car "ne résoudra pas".

Les fonctions GETDATEet SYSDATETIMEsont en effet non déterministes, mais elles sont traitées comme des constantes d'exécution pour une requête particulière. En gros, cela signifie que la valeur de la fonction est mise en cache au démarrage de l'exécution de la requête et que le résultat est réutilisé pour toutes les références dans la requête.

Aucune des «solutions de contournement» de la question n'est sûre; rien ne garantit que le comportement ne changera pas lors de la prochaine compilation du plan, lors de la prochaine application d'un service pack ou d'une mise à jour cumulative ... ou pour d'autres raisons.

La seule solution sûre consiste à utiliser un objet temporaire quelconque - une variable, une table ou une fonction multi-instructions par exemple. L'utilisation d'une solution de contournement qui semble fonctionner aujourd'hui basée sur l'observation est un excellent moyen de découvrir des comportements inattendus à l'avenir, généralement sous la forme d'une alerte de pagination à 3 heures du matin le dimanche matin.

Paul White 9
la source
"Aucune des 'solutions de contournement' dans la question n'est sûre;" idem que. Lorsque j'ai essayé d'appliquer l'un d'eux à ma requête de travail réelle, cela n'a pas aidé du tout.
JM Hicks