Pourquoi cette distribution explicite ne cause-t-elle des problèmes qu'avec un serveur lié?

21

J'interroge les données d'un serveur lié via une vue sur le serveur d'origine. La vue doit inclure quelques colonnes standardisées, telles queCreated , Modifiedet Deleted, mais dans ce cas, la table sur le serveur source n'a pas d'informations appropriées. Les colonnes sont donc explicitement converties en leurs types respectifs. J'ai mis à jour la vue, en changeant une colonne de

NULL AS Modified

à

CAST(NULL as DateTime) as Modified

Cependant, après avoir effectué cette mise à jour, la vue déclenche le message d'erreur suivant:

Msg 7341, niveau 16, état 2, ligne 3 Impossible d'obtenir la valeur de ligne actuelle de la colonne "(expression générée par l'utilisateur) .Expr1002" à partir du fournisseur OLE DB "SQLNCLI11" pour le serveur lié "".

Nous avons effectué ce changement de "distribution explicite" généralement sur le serveur d'origine sans soucis, et je soupçonne que le problème pourrait être lié à la version des serveurs impliqués. On n'a pas vraiment besoin d'appliquer ce plâtre, mais il semble plus propre. En ce moment, je suis simplement curieux de savoir pourquoi cela se produit.

Version du serveur (origine):

Microsoft SQL Server 2012 - 11.0.5058.0 (X64) 14 mai 2014 18:34:29 Copyright (c) Microsoft Corporation Enterprise Edition (64 bits) sur Windows NT 6.1 (Build 7601: Service Pack 1) (Hyperviseur)

Version du serveur (lié):

Microsoft SQL Server 2008 R2 (SP1) - 10.50.2500.0 (X64) 17 juin 2011 00:54:03 Copyright (c) Microsoft Corporation Enterprise Edition (64 bits) sur Windows NT 6.1 (Build 7601: Service Pack 1) (Hyperviseur )

modifier
Je viens de réaliser que j'ai fait une erreur en ne publiant pas toutes les colonnes en question, et je dois m'excuser d'avoir omis un détail important. Je ne sais pas comment je n'ai pas remarqué cela plus tôt. La question demeure cependant.

La conversion erronée ne se produit pas avec la conversion en DateTime, mais avec une colonne en cours de conversion en UniqueIdentifier.

C'est le coupable:

CAST(NULL AS UniqueIdentifier) AS [GUID]

Les identificateurs uniques sont pris en charge sur SQL Server 2008 R2 et, comme mentionné dans les commentaires, la requête effectuée par la vue s'exécute correctement sur le serveur lié.

krystah
la source
Avez-vous un paramètre ANSI NULL différent sur chaque serveur? Collation différente?
Randolph West
Les deux serveurs ont ANSI NULL = 0. Le serveur d'origine a le classement Danish_Norwegian_CI_ASet le serveur lié a le classement SQL_Danish_Pref_CP1_CI_AS, mais la COLLATEclause ne peut pas être appliquée aux DateTimecolonnes, donc je ne suis pas allé beaucoup plus loin!
krystah
Est-ce que cela échouerait en cas de select Null from ...requête WITH ou imbriquée et CASTdans une autre?
Stoleg
Sans la distribution explicite, il sera traité comme INTsi vous avez modifié le type de données en le faisant. Je ne sais pas pourquoi cela vous donnerait ce message d'erreur.
Martin Smith
J'avais précédemment essayé d'encapsuler les valeurs sélectionnées avec des valeurs réelles dans un CTE, puis de les sélectionner et de virer de bord sur les NULL castés dans l'instruction suivant le CTE sans chance. J'ai essayé votre suggestion, en conservant les valeurs NULL dans le CTE et en les transposant dans l'instruction interrogeant le CTE, mais cela donne également la même erreur.
krystah

Réponses:

13

Ainsi, j'ai pu reproduire l'erreur après avoir réalisé que cela CASTse faisait localement, pas sur l'instance distante. J'avais précédemment recommandé de passer au SP3 dans l'espoir de résoudre ce problème (en partie parce que je ne pouvais pas reproduire l'erreur sur SP3, et en partie parce que c'était une bonne idée malgré tout). Cependant, maintenant que je peux reproduire l'erreur, il est clair que passer au SP3, bien que probablement une bonne idée, ne va pas résoudre ce problème. Et j'ai également reproduit l'erreur dans SQL Server 2008 R2 RTM et 2014 SP1 (en utilisant un serveur lié local "loop-back" dans les trois cas).

Il semble que ce problème soit lié à emplacement d' exécution de la requête, ou au moins à l'exécution de certaines parties . Je dis cela parce que j'ai réussi à faire CASTfonctionner l' opération, mais uniquement en incluant une référence à un objet DB local:

SELECT rmt.*, CAST(NULL AS UNIQUEIDENTIFIER) AS [GUID]
FROM [Local].[database_name].[dbo].[table_name] rmt
CROSS JOIN (SELECT TOP (1) 1 FROM [sys].[data_spaces]) tmp(dummy);

Cela fonctionne réellement. Mais ce qui suit obtient l'erreur d'origine:

SELECT rmt.*, CAST(NULL AS UNIQUEIDENTIFIER) AS [GUID]
FROM [Local].[database_name].[dbo].[table_name] rmt
CROSS JOIN (VALUES (1)) tmp(dummy);

Je suppose que lorsqu'il n'y a pas de références locales, l'intégralité de la requête est expédiée vers le système distant pour être exécutée, et pour une raison quelconque, NULLil ne peut pas être converti en UNIQUEIDENTIFIER, ou peut-être que le NULLpilote OLE DB n'est pas traduit correctement.


Sur la base des tests que j'ai effectués, cela semble être un bogue, mais je ne sais pas si le bogue se trouve dans SQL Server ou le pilote SQL Server Native Client / OLEDB. Cependant, l'erreur de conversion se produit dans le pilote OLEDB et n'est donc pas nécessairement un problème de conversion à partir deINT vers UNIQUEIDENTIFIER(une conversion qui n'est pas autorisée dans SQL Server) car le pilote n'utilise pas SQL Server pour effectuer des conversions (SQL Server ne fait pas non plus permettre la conversion INTen DATE, mais le pilote OLEDB gère cela avec succès, comme indiqué dans l'un des tests).

J'ai effectué trois tests. Pour les deux qui ont réussi, j'ai regardé les plans d'exécution XML qui montrent la requête qui est exécutée à distance. Pour les trois, j'ai capturé toutes les exceptions ou événements OLEDB via SQL Profiler:

Événements:

  • Erreurs et avertissements
    • Attention
    • Exception
    • Avertissements d'exécution
    • Message d'erreur utilisateur
  • OLEDB
    • tout
  • TSQL
    • tous sauf :
      • SQL: StmtRecompile
      • Type statique XQuery

Filtres de colonne:

  • Nom de l'application
    • PAS COMME % Intellisense%
  • SPID
    • Supérieur ou égal à 50

LES TESTS

  • Test 1

    • CAST(NULL AS UNIQUEIDENTIFIER) ça marche

    SELECT TOP (2) CAST(NULL AS UNIQUEIDENTIFIER) AS [Something]
                 , (SELECT COUNT(*) FROM sys.[data_spaces]) AS [lcl]
    FROM [Local].[TEMPTEST].[sys].[objects] rmt;

    Partie pertinente du plan d'exécution XML:

              <DefinedValue>
                <ColumnReference Column="Expr1002" />
                <ScalarOperator ScalarString="NULL">
                  <Const ConstValue="NULL" />
                </ScalarOperator>
              </DefinedValue>
      ...
    <RemoteQuery RemoteSource="Local" RemoteQuery=
     "SELECT 1 FROM &quot;TEMPTEST&quot;.&quot;sys&quot;.&quot;objects&quot; &quot;Tbl1001&quot;"
     />
  • Test 2

    • CAST(NULL AS UNIQUEIDENTIFIER) qui échoue

    SELECT TOP (2) CAST(NULL AS UNIQUEIDENTIFIER) AS [Something]
             --  , (SELECT COUNT(*) FROM sys.[data_spaces]) AS [lcl]
    FROM [Local].[TEMPTEST].[sys].[objects] rmt;

    (Remarque: j'ai conservé la sous-requête là-dedans, commenté, de sorte que ce serait une différence de moins lorsque je comparais les fichiers de trace XML)

  • Test 3

    • CAST(NULL AS DATE) ça marche

    SELECT TOP (2) CAST(NULL AS DATE) AS [Something]
             --  , (SELECT COUNT(*) FROM sys.[data_spaces]) AS [lcl]
    FROM [Local].[TEMPTEST].[sys].[objects] rmt;

    (Remarque: j'ai conservé la sous-requête là-dedans, commenté, de sorte que ce serait une différence de moins lorsque je comparais les fichiers de trace XML)

    Partie pertinente du plan d'exécution XML:

              <DefinedValue>
                <ColumnReference Column="Expr1002" />
                <ScalarOperator ScalarString="[Expr1002]">
                  <Identifier>
                    <ColumnReference Column="Expr1002" />
                  </Identifier>
                </ScalarOperator>
              </DefinedValue>
     ...
    <RemoteQuery RemoteSource="Local" RemoteQuery=
     "SELECT TOP (2) NULL &quot;Expr1002&quot; FROM &quot;TEMPTEST&quot;.&quot;sys&quot;.&quot;objects&quot; &quot;Tbl1001&quot;" 
     />

Si vous regardez le test n ° 3, il fait un SELECT TOP (2) NULLsur le système "distant". La trace du Générateur de profils SQL montre que le type de données de ce champ distant est en fait INT. La trace montre également que le champ côté client (c'est-à-dire où j'exécute la requête) est DATE, comme prévu. La conversion de INTà DATE, quelque chose qui obtiendra une erreur dans SQL Server, fonctionne très bien dans le pilote OLEDB. La valeur distante est NULL, elle est donc retournée directement, d'où<ColumnReference Column="Expr1002" /> .

Si vous regardez le test n ° 1, il fait un SELECT 1sur le système "distant". La trace du Générateur de profils SQL montre que le type de données de ce champ distant est en fait INT. La trace montre également que le champ côté client (c'est-à-dire où j'exécute la requête) est GUID, comme prévu. La conversion de INTà GUID(rappelez-vous, cela se fait dans le pilote, et OLEDB l'appelle "GUID"), quelque chose qui obtiendra une erreur dans SQL Server, fonctionne très bien dans le pilote OLEDB. La valeur distante ne l'est pas NULL , elle est donc remplacée par un littéral NULL, d'où<Const ConstValue="NULL" /> .

Le test n ° 2 échoue, il n'y a donc pas de plan d'exécution. Cependant, il interroge le système "distant" avec succès, mais ne peut tout simplement pas renvoyer l'ensemble de résultats. La requête capturée par SQL Profiler est:

SELECT TOP (2) NULL "Expr1002" FROM "TEMPTEST"."sys"."objects" "Tbl1001"

C'est exactement la même requête qui est effectuée dans le test n ° 1, mais ici, elle échoue. Il existe d'autres différences mineures, mais je ne peux pas interpréter complètement la communication OLEDB. Cependant, le champ distant s'affiche toujours sous la forme INT(wType = 3 = adInteger / entier signé sur quatre octets / DBTYPE_I4) tandis que le champ "client" s'affiche toujours sous la forme GUID(wType = 72 = adGUID / identificateur global unique / DBTYPE_GUID). La documentation OLE DB ne nous aide pas beaucoup comme GUID Type de données Conversions , DBDATE Type de données Conversions et I4 des types de données Conversions montrent que la conversion de I4 soit GUID ou DBDATE est non pris en charge, mais les DATEtravaux de la requête.

Les fichiers XML de trace pour les trois tests se trouvent sur PasteBin. Si vous voulez voir les détails de la différence entre chaque test et les autres, vous pouvez les enregistrer localement puis faire un "diff" sur eux. Les fichiers sont:

  1. NullGuidSuccess.xml
  2. NullGuidError.xml
  3. NullDateSuccess.xml

ERGO?

Que faire à ce sujet? Probablement juste la solution de contournement que j'ai notée dans la section supérieure, étant donné que SQL Native Client - SQLNCLI11- est obsolète à partir de SQL Server 2012. La plupart des pages MSDN sur le sujet de SQL Server Native Client ont l'avis suivant sur le Haut:

Attention

SQL Server Native Client (SNAC) n'est pas pris en charge au-delà de SQL Server 2012. Évitez d'utiliser SNAC dans les nouveaux travaux de développement et prévoyez de modifier les applications qui l'utilisent actuellement. Le pilote Microsoft ODBC pour SQL Server fournit une connectivité native de Windows à Microsoft SQL Server et à Microsoft Azure SQL Database.

Pour plus d'informations, veuillez consulter:


ODBC ??

J'ai configuré un serveur lié ODBC via:

EXEC master.dbo.sp_addlinkedserver
  @server = N'LocalODBC',
  @srvproduct=N'{my_server_name}',
  @provider=N'MSDASQL',
  @provstr=N'Driver={SQL Server};Server=(local);Trusted_Connection=Yes;';

EXEC master.dbo.sp_addlinkedsrvlogin
  @rmtsrvname=N'LocalODBC',
  @useself=N'True',
  @locallogin=NULL,
  @rmtuser=NULL,
  @rmtpassword=NULL;

Et puis essayé:

SELECT CAST(NULL AS UNIQUEIDENTIFIER) AS [Something]
FROM [LocalODBC].[tempdb].[sys].[objects] rmt;

et a reçu l'erreur suivante:

Le fournisseur OLE DB "MSDASQL" pour le serveur lié "LocalODBC" a renvoyé le message "La conversion demandée n'est pas prise en charge.".
Msg 7341, niveau 16, état 2, ligne 53
Impossible d'obtenir la valeur de ligne actuelle de la colonne "(expression générée par l'utilisateur) .Expr1002" à partir du fournisseur OLE DB "MSDASQL" pour le serveur lié "LocalODBC".


PS

En ce qui concerne le transport des GUID entre les serveurs distants et locaux, les valeurs non NULL sont gérées via une syntaxe spéciale. J'ai remarqué les informations d'événement OLE DB suivantes dans la trace du Générateur de profils SQL lorsque j'ai exécuté CAST(0x00 AS UNIQUEIDENTIFIER):

<RemoteQuery RemoteSource="Local" RemoteQuery=
 "SELECT {guid'00000000-0000-0000-0000-000000000000'} &quot;Expr1002&quot; FROM &quot;TEMPTEST&quot;.&quot;sys&quot;.&quot;objects&quot; &quot;Tbl1001&quot;" 
 />

PPS

J'ai également testé via OPENQUERYavec la requête suivante:

SELECT TOP (2) CAST(NULL AS UNIQUEIDENTIFIER) AS [Something]
     --, (SELECT COUNT(*) FROM sys.[data_spaces]) AS [lcl]
FROM   OPENQUERY([Local], N'SELECT 705 AS [dummy] FROM [TEMPTEST].[sys].[objects];') rmt;

et il a réussi, même sans la référence d'objet local. Le fichier XML de trace de SQL Profiler a été publié dans PasteBin à l'adresse suivante:

NullGuidSuccessOPENQUERY.xml

Le plan d'exécution XML le montre en utilisant une NULLconstante, comme dans le test n ° 1.

Solomon Rutzky
la source
J'ai recréé ce problème sur 2012 sp3-cu1.
Bob Klimes
@BobKlimes Intéressant. S'agit-il d'un serveur lié à une instance ou à un bouclage 2008 R2?
Solomon Rutzky
le serveur lié à distance est 2008 R2 sp2 + MS15-058 (10.50.4339.0).
Bob Klimes
1
ne semble pas être lié à la version. Ont testé plusieurs combos de 2008r2,2012,2014,2016 et tous ont jusqu'à présent produit des erreurs, même 2008r2-2008r2.
Bob Klimes
2
Quel problème extrêmement obscur. Merci beaucoup pour la perspicacité et la recherche, @srutzky, c'est grandement apprécié. Je garderai à l'esprit la décrépitude du SNAC pour les travaux futurs et le retour à la solution de contournement susmentionnée. Bon travail!
krystah
4

Il n'y a qu'une solution de contournement laide - utilisez une constante de date comme '1900-01-01'au lieu de null.

CAST('1900-01-01' as DateTime) as Modified

Après l'importation, vous pouvez mettre à jour les colonnes avec 1900-01-01retour à Null.

C'est une sorte de fonctionnalité / bogue de SQL 2012 comme ici .

Modifier: remplacé 1900-00-00par une date valide 1900-01-01selon le commentaire @a_horse_with_no_name ci-dessous.

Anton Krouglov
la source
Cette solution de contournement méritait probablement d'être mentionnée, mais elle n'est peut-être plus pertinente maintenant que le PO a précisé que le coupable du problème est une uniqueidentifiercolonne. Ou peut-être pourrait-on l'adapter - quelque chose comme CAST('00000000-0000-0000-0000-000000000000' AS UniqueIdentifier) AS [GUID], peut-être?
Andriy M
Merci les gars. La diffusion vers un GUID ou DateTime vide fonctionne, mais je dois comprendre pourquoi cela se produit. Il convient également de mentionner que je n'importe rien, il n'y a donc aucune possibilité de modifier les données source.
krystah
1
1900-00-00est une date invalide et ne sera pas acceptée.
a_horse_with_no_name
@a_horse_with_no_name: erreur stupide, corrigé.
Anton Krouglov
2

Le problème est lié aux conversions de types de données (comme indiqué dans les commentaires).

Considérer ce qui suit:

SELECT NULL as NullColumn INTO SomeTable;
EXEC sp_help SomeTable;
DROP TABLE SomeTable;

Notez que le NullColumnest de type int. SQL Server n'aime pas convertir les intvaleurs en uniqueidentifier. Cette SELECTinstruction échouera lors d'une conversion de type de données:

--Just a SELECT from nothing
SELECT CAST(CAST(NULL as int) as uniqueidentifier);
--
--or to see it from a physical table:
SELECT NULL as NullColumn INTO SomeTable;
SELECT CAST(NullColumn as uniqueidentifier) FROM SomeTable;
DROP TABLE SomeTable;

Msg 529, niveau 16, état 2, ligne 3

La conversion explicite du type de données int en identifiant unique n'est pas autorisée.

Bien que cette valeur spécifique (NULL) puisse être convertie en GUID, SQL Server renvoie l'erreur en fonction de la conversion du type de données, avant même de regarder les valeurs spécifiques. Au lieu de cela, vous devrez effectuer une CASTopération en plusieurs étapes pour modifier l'implicite inten un type de données qui peut être converti proprement en uniqueidentifer- ce qui signifie couler d'abord pour varcharensuite uniqueidentifier:

--Just a SELECT from nothing
SELECT CAST(CAST(CAST(NULL as int) as varchar) as uniqueidentifier);
--
--or to see it from a physical table:
SELECT NULL as NullColumn INTO SomeTable;
SELECT CAST(CAST(NullColumn as varchar(32)) as uniqueidentifier) FROM SomeTable;
DROP TABLE SomeTable;
AMtwo
la source
Ce problème n'est pas vraiment dû aux conversions de types de données, du moins pas dans SQL Server. Les détails sont dans ma réponse , mais la conversion se fait dans le pilote OLEDB, pas dans SQL Server, et les règles de conversion ne sont pas les mêmes.
Solomon Rutzky
1

Le PO peut finalement décider s'il s'agit d'une réponse appropriée.

Je n'ai pas de preuve `` absolue '', mais je `` soupçonne '' que le problème provient du fait qu'un UniqueIdentifer dépend du serveur et peut-être que le fournisseur a du mal à déterminer de quel serveur (local ou distant) il peut obtenir cet identifiant unique, même s'il est nul. C'est pourquoi vous pouvez probablement convertir n'importe quel autre type de données avec succès dans ce scénario, mais pas l'identificateur unique. Les types de données qui dépendent du «serveur» comme UNIQUEIDENTIFIERS et DATETIMEOFFSET vous donneront l'erreur que vous rencontrez.

L'utilisation d'OPENQUERY au lieu d'un nom en 4 parties fonctionne.

set nocount on  
DECLARE @cmd nVARCHAR(max)
DECLARE @datatype SYSNAME

DECLARE _CURSOR CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT NAME
FROM sys.types 

OPEN _CURSOR

FETCH NEXT
FROM _CURSOR
INTO @datatype

WHILE @@FETCH_STATUS = 0
BEGIN
    BEGIN TRY
        SET @cmd = 'select top 1 cast(null as ' + @Datatype + ') as CastedData from remoteserver.remotedatabase.remoteschema.remotetable'
        PRINT @cmd
        EXECUTE sp_executesql @cmd
    END TRY

    BEGIN CATCH
        PRINT Error_message()
    END CATCH

FETCH NEXT
FROM _CURSOR
INTO @datatype
END --End While

CLOSE _CURSOR

DEALLOCATE _CURSOR
Scott Hodgin
la source
Qu'entendez-vous exactement par Uniqueidentifieret DateTimeOffsetdépendant du serveur? Avez-vous des sources pour cela?
krystah
@krystah - À partir de ce lien ( technet.microsoft.com/en-us/library/ms190215(v=sql.105).aspx ) "Le type de données uniqueidentifier stocke des valeurs binaires de 16 octets qui fonctionnent comme des identificateurs globaux uniques (GUID) . Un GUID est un numéro binaire unique. Aucun autre ordinateur au monde ne générera de doublon de cette valeur de GUID. L'utilisation principale d'un GUID consiste à attribuer un identifiant qui doit être unique dans un réseau comportant de nombreux ordinateurs sur de nombreux sites. "
Scott Hodgin
Pour DateTimeOffSet ( msdn.microsoft.com/en-us/library/bb630289.aspx ) Définit une date qui est combinée avec une heure d'une journée qui a connaissance du fuseau horaire et est basée sur une horloge de 24 heures
Scott Hodgin
Je «déduis» que, puisque ces deux types de données «semblent» être les seuls à donner l'erreur dans votre scénario et que ces types de données sont «dépendants du serveur», c'est la raison de votre problème. Si vous utilisez OPENQUERY pour récupérer vos résultats à partir de votre vue et ajouter les conversions nulles supplémentaires, cela devrait fonctionner parce que le fournisseur sait `` où '' obtenir ces informations
Scott Hodgin
@krystah et Scott: ces deux types de données ne dépendent pas du serveur, du moins pas dans la façon dont vous décrivez et sous-entendez. Les GUID dépendent de "l'architecture" en termes de leur représentation binaire sous-jacente (voir ici pour plus de détails), mais si c'était un problème ici, alors aucun GUID ne serait transféré correctement. Pour DATETIMEOFFSET, qui ne connaît qu'un décalage, pas un fuseau horaire / TimeZoneID réel. Bien que les deux utilisent les informations système pour générer de nouvelles valeurs, aucune nouvelle valeur n'est générée ici, et si elles l'étaient, elles ne seraient pas générées dans le fournisseur.
Solomon Rutzky
0

Solution: la réponse acceptée semble indiquer que la conversion doit avoir lieu localement car le pilote OLEDB ne la prend pas en charge.

Je pense donc qu'une solution de contournement simple (au moins dans le cas de ma requête qui sélectionne un null uniqueidentifierdans le cas de base d'un CTE récursif) consiste à déclarer une variable nulle:

declare @nullGuid as uniqueidentifier = null;

--Instead of...
CAST(NULL AS UniqueIdentifier) AS [GUID]

--use
@nullGuid AS [GUID]
xr280xr
la source