web-dev-qa-db-fra.com

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

J'interroge les données d'un serveur lié via une vue sur le serveur Origin. La vue doit inclure quelques colonnes standardisées, telles que Created, Modified et Deleted, mais dans ce cas, la table sur le serveur source n'a pas de Info. 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" de OLE fournisseur de base de données "SQLNCLI11" pour le serveur lié "".

Nous avons effectué ce changement de "distribution explicite" généralement sur le serveur Origin sans soucis, et je soupçonne que le problème pourrait être lié à la version des serveurs impliqués. Nous n'avons pas vraiment besoin pour appliquer cette distribution, mais cela 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 convertie en UniqueIdentifier.

Ceci 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é.

21
krystah

Ainsi, j'ai pu reproduire l'erreur après avoir réalisé que le CAST était fait 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 le passage 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 "bouclé" dans les trois cas).

Il semble que ce problème soit lié à la requête est en cours d'exécution, ou au moins à l'endroit où partie (s) est en cours d'exécution. Je dis cela parce que j'ai réussi à faire fonctionner l'opération CAST, 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, la requête entière est envoyée au système distant pour être exécutée, et pour une raison quelconque, NULLs ne peut pas être converti en UNIQUEIDENTIFIER, ou peut-être le NULL est traduit par le pilote OLE DB de manière incorrecte.


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 de INT vers UNIQUEIDENTIFIER (une conversion qui n'est pas autorisée dans SQL Server) car le pilote n'est pas utiliser SQL Server pour effectuer des conversions (SQL Server ne permet pas non plus de convertir INT en 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 ESSAIS

  • Test 1

    • CAST(NULL AS UNIQUEIDENTIFIER) qui fonctionne
    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;
    

    (note: j'ai gardé 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) qui fonctionne
    SELECT TOP (2) CAST(NULL AS DATE) AS [Something]
             --  , (SELECT COUNT(*) FROM sys.[data_spaces]) AS [lcl]
    FROM [Local].[TEMPTEST].[sys].[objects] rmt;
    

    (note: j'ai gardé 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 une SELECT TOP (2) NULL sur 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 d'où j'exécute la requête) est DATE, comme prévu. La conversion de INT en 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ù le <ColumnReference Column="Expr1002" />.

Si vous regardez le test n ° 1, il fait un SELECT 1 Sur 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 d'où j'exécute la requête) est GUID, comme prévu. La conversion de INT en 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 est pasNULL, elle est donc remplacée par un littéral NULL, d'où le <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. Toutefois, 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 n'aide pas beaucoup --- GUID Data Type Conversions , DBDATE Data Type Conversions , and I4 Data Type Conversions montre que la conversion de I4 en un [~ # ~] guid [~ # ~] ou [~ # ~] dbdate [~ # ~] n'est pas pris en charge, mais le DATE fonctionne.

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 et ensuite 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 depuis SQL Server 2012. La plupart des pages MSDN sur le sujet de SQL Server Native Client avoir l'avis suivant en haut:

Avertissement

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. 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 ODBC serveur lié 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" de OLE fournisseur de base de données "MSDASQL" pour le serveur lié "LocalODBC".


P.S.

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 suivantes OLE DB Event 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;" 
 />

P.P.S.

J'ai également testé via OPENQUERY avec 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 constante NULL, comme dans le test n ° 1.

13
Solomon Rutzky

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-01 retour à Null.

C'est une sorte de fonctionnalité/bug de SQL 2012 selon ici .

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

4
Anton Krouglov

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 NullColumn est de type int. SQL Server n'aime pas convertir les valeurs de int en uniqueidentifier. Cette instruction SELECT é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 opération CAST en plusieurs étapes pour modifier le int implicite en un type de données qui peut être converti proprement en uniqueidentifer--, ce qui signifie la conversion en premier à varchar puis à 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;
2
AMtwo

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

Je n'ai aucune preuve `` absolue '', mais je `` soupçonne '' que le problème vient 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) 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'identifiant 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
1
Scott Hodgin

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 uniqueidentifier dans le cas de base d'un CTE récursif) consiste à déclarer une variable null:

declare @nullGuid as uniqueidentifier = null;

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

--use
@nullGuid AS [GUID]
0
xr280xr