¿Por qué este reparto explícito causa problemas solo con un servidor vinculado?

21

Estoy consultando datos de un servidor vinculado a través de una vista en el servidor de origen. La vista debe incluir un par de columnas estandarizadas, como Created, Modifiedy Deleted, pero en este caso la tabla en el servidor de origen no tiene ninguna información adecuada. Por lo tanto, las columnas se convierten explícitamente a sus respectivos tipos. Actualicé la vista, cambiando una columna de

NULL AS Modified

a

CAST(NULL as DateTime) as Modified

Sin embargo, después de realizar esta actualización, la vista está activando el siguiente mensaje de error:

Mensaje 7341, Nivel 16, Estado 2, Línea 3 No se puede obtener el valor de fila actual de la columna "(expresión generada por el usuario) .Expr1002" del proveedor OLE DB "SQLNCLI11" para el servidor vinculado "".

Hemos realizado este cambio de "conversión explícita" generalmente en el servidor de origen sin preocupaciones, y sospecho que el problema podría estar relacionado con la versión de los servidores involucrados. Realmente no necesitamos aplicar este yeso, pero se siente más limpio. En este momento tengo curiosidad de saber por qué sucede esto.

Versión del servidor (origen):

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

Versión del servidor (vinculado):

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

Editar
Me acabo de dar cuenta de que cometí un error al no publicar todas las columnas en cuestión, y debo disculparme por dejar de lado un detalle importante. No sé cómo no me di cuenta de esto antes. Sin embargo, la pregunta aún permanece.

La conversión errónea no ocurre con la conversión a DateTime, sino con una columna que se convierte a UniqueIdentifier.

Este es el culpable:

CAST(NULL AS UniqueIdentifier) AS [GUID]

UniqueIdentifiers son compatibles con SQL Server 2008 R2 y, como se menciona en los comentarios, la consulta realizada por la vista se ejecuta bien en el servidor vinculado.

krystah
fuente
¿Tiene una configuración NULL ANSI diferente en cada servidor? Diferente cotejo?
Randolph West el
Ambos servidores tienen ANSI NULL = 0. El servidor de origen tiene la intercalación Danish_Norwegian_CI_ASy el servidor vinculado tiene la intercalación SQL_Danish_Pref_CP1_CI_AS, pero la COLLATEcláusula no se puede aplicar a las DateTimecolumnas, por lo que no he avanzado mucho.
krystah
¿Fallaría esto si tuviera select Null from ...CON o consulta anidada y CASTen otra?
Stoleg
Sin la conversión explícita, se tratará como INTsi hubiera cambiado el tipo de datos al hacerlo. Sin embargo, no sé por qué eso le daría ese mensaje de error.
Martin Smith
Anteriormente intenté ajustar los valores seleccionados con los valores reales en un CTE, luego los seleccioné y agregué los NULL emitidos en la declaración que sigue al CTE sin suerte. Intenté su sugerencia, manteniendo los NULL en el CTE y emitiéndolos en la declaración que consulta el CTE, pero también produce el mismo error.
krystah

Respuestas:

13

Entonces, pude reproducir el error después de darme cuenta de que CASTse estaba haciendo localmente, no en la instancia remota. Anteriormente había recomendado pasar al SP3 con la esperanza de solucionarlo (en parte debido a que no podía reproducir el error en SP3 y en parte debido a que era una buena idea, independientemente). Sin embargo, ahora que puedo reproducir el error, está claro que pasar al SP3, aunque probablemente sea una buena idea, no va a solucionarlo. Y también reproduje el error en SQL Server 2008 R2 RTM y 2014 SP1 (utilizando un servidor vinculado local "en bucle" en los tres casos).

Parece que este problema tiene que ver con dónde se está ejecutando la consulta, o al menos dónde se están ejecutando parte (s) de la misma. Digo esto porque pude hacer que la CASToperación funcione, pero solo al incluir una referencia a un objeto 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);

Eso realmente funciona. Pero lo siguiente obtiene el error original:

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

Supongo que cuando no hay referencias locales, toda la consulta se envía al sistema remoto para que se ejecute, y por alguna razón NULLno se puede convertir UNIQUEIDENTIFIER, o tal vez el NULLcontrolador OLE DB lo está traduciendo incorrectamente.


Según las pruebas que he realizado, esto parecería ser un error, pero no estoy seguro de si el error se encuentra en SQL Server o en el controlador SQL Server Native Client / OLEDB. Sin embargo, el error de conversión ocurre dentro del controlador OLEDB, por lo que no es necesariamente un problema de conversión de INTa UNIQUEIDENTIFIER(una conversión que no está permitida en SQL Server) ya que el controlador no está usando SQL Server para realizar conversiones (SQL Server tampoco permite la conversión INTa DATE, sin embargo, el controlador OLEDB lo maneja con éxito, como se muestra en una de las pruebas).

Hice tres pruebas. Para los dos que tuvieron éxito, miré los planes de ejecución XML que muestran la consulta que se ejecuta de forma remota. Para los tres, capturé cualquier excepción o evento OLEDB a través de SQL Profiler:

Eventos:

  • Errores y advertencias
    • Atención
    • Excepción
    • Advertencias de Ejecución
    • Mensaje de error del usuario
  • OLEDB
    • todos
  • TSQL
    • todos excepto :
      • SQL: StmtRecompile
      • Tipo estático XQuery

Filtros de columna:

  • Nombre de la aplicación
    • NO ME GUSTA % Intellisense%
  • SPID
    • Mayor o igual a 50

LOS EXÁMENES

  • Prueba 1

    • CAST(NULL AS UNIQUEIDENTIFIER) eso funciona

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

    Porción relevante del plan de ejecución 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;"
     />
  • Prueba 2

    • CAST(NULL AS UNIQUEIDENTIFIER) eso falla

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

    (nota: mantuve la subconsulta allí, comenté, de modo que sería una diferencia menos cuando comparé los archivos de rastreo XML)

  • Prueba 3

    • CAST(NULL AS DATE) eso funciona

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

    (nota: mantuve la subconsulta allí, comenté, de modo que sería una diferencia menos cuando comparé los archivos de rastreo XML)

    Porción relevante del plan de ejecución 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 nos fijamos en la Prueba # 3, está haciendo un SELECT TOP (2) NULLen el sistema "remoto". La traza del Analizador de SQL muestra que el tipo de datos de este campo remoto es de hecho INT. El seguimiento también muestra que el campo en el lado del cliente (es decir, desde donde estoy ejecutando la consulta) es DATE, como se esperaba. La conversión de INTa DATE, algo que obtendrá un error en SQL Server, funciona bien dentro del controlador OLEDB. El valor remoto es NULL, por lo que se devuelve directamente, de ahí el <ColumnReference Column="Expr1002" />.

Si nos fijamos en la Prueba # 1, está haciendo un SELECT 1en el sistema "remoto". La traza del Analizador de SQL muestra que el tipo de datos de este campo remoto es de hecho INT. El seguimiento también muestra que el campo en el lado del cliente (es decir, desde donde estoy ejecutando la consulta) es GUID, como se esperaba. La conversión de INTa GUID(recuerde, esto se realiza dentro del controlador, y OLEDB lo llama "GUID"), algo que obtendrá un error en SQL Server, funciona bien dentro del controlador OLEDB. El valor remoto no es NULL, por lo que se reemplaza con un literal NULL, de ahí el <Const ConstValue="NULL" />.

La prueba # 2 falla, por lo que no hay un plan de ejecución. Sin embargo, consulta el sistema "remoto" con éxito, pero simplemente no puede devolver el conjunto de resultados. La consulta que capturó SQL Profiler es:

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

Esa es exactamente la misma consulta que se está haciendo en la Prueba # 1, pero aquí está fallando. Hay otras diferencias menores, pero no puedo interpretar completamente la comunicación OLEDB. Sin embargo, el campo remoto todavía se muestra como INT(wType = 3 = adInteger / entero de cuatro bytes con signo / DBTYPE_I4) mientras que el campo "cliente" todavía se muestra como GUID(wType = 72 = adGUID / identificador único global / DBTYPE_GUID). La documentación de OLE DB no ayuda mucho, ya que las conversiones de tipo de datos GUID , las conversiones de tipo de datos DBDATE y las conversiones de tipo de datos I4 muestran que la conversión de I4 a GUID o DBDATE no es compatible, pero la DATEconsulta funciona.

Los archivos XML Trace para las tres pruebas se encuentran en PasteBin. Si desea ver los detalles de dónde difiere cada prueba de las demás, puede guardarlas localmente y luego hacer una "diferencia" en ellas. Los archivos son:

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

¿ES DECIR?

¿Qué hacer al respecto? Probablemente solo la solución que anoté en la sección superior, dado que el SQL Native Client - SQLNCLI11está en desuso a partir de SQL Server 2012. La mayoría de las páginas de MSDN sobre el tema del SQL Server Native Client tienen el siguiente aviso en el parte superior:

Advertencia

SQL Server Native Client (SNAC) no es compatible más allá de SQL Server 2012. Evite usar SNAC en nuevos trabajos de desarrollo y planee modificar las aplicaciones que actualmente lo usan. El controlador ODBC de Microsoft para SQL Server proporciona conectividad nativa de Windows a Microsoft SQL Server y Microsoft Azure SQL Database.

Para más información, consulte:


ODBC ??

Configuré un servidor vinculado ODBC a través de:

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;

Y luego probé:

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

y recibió el siguiente error:

El proveedor OLE DB "MSDASQL" para el servidor vinculado "LocalODBC" devolvió el mensaje "La conversión solicitada no es compatible".
Mensaje 7341, Nivel 16, Estado 2, Línea 53
No se puede obtener el valor de fila actual de la columna "(expresión generada por el usuario) .Expr1002" del proveedor OLE DB "MSDASQL" para el servidor vinculado "LocalODBC".


PD

En lo que se refiere al transporte de GUID entre servidores remotos y locales, los valores no NULL se manejan mediante una sintaxis especial. Noté la siguiente información del evento OLE DB en el seguimiento del Analizador de SQL cuando ejecuté 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

También probé a través OPENQUERYde la siguiente consulta:

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;

y tuvo éxito, incluso sin la referencia de objeto local. El archivo XML de seguimiento de SQL Profiler se ha publicado en PasteBin en:

NullGuidSuccessOPENQUERY.xml

El plan de ejecución XML lo muestra usando una NULLconstante, igual que en la Prueba # 1.

Solomon Rutzky
fuente
He recreado este problema en 2012 sp3-cu1.
Bob Klimes
@BobKlimes Interesante. ¿Está usando un servidor vinculado a una instancia 2008 R2 o un bucle de retorno?
Solomon Rutzky
el servidor vinculado remoto es 2008 R2 sp2 + MS15-058 (10.50.4339.0).
Bob Klimes
1
no parece estar relacionado con la versión. He probado múltiples combos de 2008r2,2012,2014,2016 y todos han producido errores hasta ahora, incluso 2008r2-2008r2.
Bob Klimes
2
Qué tema extremadamente oscuro. Muchas gracias por el conocimiento y la investigación, @srutzky, es muy apreciado. Voy a tener en cuenta la disminución de SNAC para futuros trabajos y recurrir a la solución antes mencionada. ¡Buen trabajo!
krystah
4

Solo hay una solución fea: use una constante de fecha como en '1900-01-01'lugar de null.

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

Después de la importación, puede actualizar las columnas con 1900-01-01Back to Null.

Esta es una especie de característica / error de SQL 2012 según aquí .

Editar: reemplazado 1900-00-00con una fecha válida 1900-01-01según el comentario @a_horse_with_no_name a continuación.

Anton Krouglov
fuente
Probablemente valga la pena mencionar esta solución, pero puede que ya no sea relevante ahora que el OP ha aclarado que el culpable del problema es una uniqueidentifiercolumna. O tal vez podría adaptarse, ¿algo así CAST('00000000-0000-0000-0000-000000000000' AS UniqueIdentifier) AS [GUID], tal vez?
Andriy M
Gracias chicos. La conversión a un GUID o DateTime vacío funciona, pero necesito entender por qué sucede esto. También vale la pena mencionar que no estoy importando nada, por lo que no hay posibilidad de alterar los datos de origen.
krystah
1
1900-00-00es una fecha inválida y no será aceptada.
a_horse_with_no_name
@a_horse_with_no_name: error estúpido, corregido.
Anton Krouglov
2

El problema está relacionado con las conversiones de tipos de datos (como se indica en los comentarios).

Considera lo siguiente:

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

Tenga en cuenta que el NullColumnes de tipo int. A SQL Server no le gusta convertir intvalores a uniqueidentifier. Esta SELECTdeclaración fallará en una conversión de tipo de datos:

--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;

Mensaje 529, Nivel 16, Estado 2, Línea 3

La conversión explícita del tipo de datos int a uniqueidentifier no está permitida.

Si bien este valor específico (NULL) se puede convertir a un GUID, SQL Server arroja el error en función de la conversión del tipo de datos, incluso antes de mirar los valores específicos. En su lugar, deberá realizar una CASToperación de varios pasos para cambiar lo implícito inta un tipo de datos que se pueda convertir limpiamente en, lo que uniqueidentifersignifica enviar primero varchara 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
fuente
Este problema no se debe realmente a las conversiones de tipos de datos, al menos no dentro de SQL Server. Los detalles están en mi respuesta , pero la conversión se realiza dentro del controlador OLEDB, no dentro de SQL Server, y las reglas de conversión no son las mismas.
Solomon Rutzky
1

El OP finalmente puede decidir si esta es una respuesta apropiada.

No tengo pruebas "absolutas", pero "sospecho" que el problema se debe al hecho de que un UniqueIdentifer depende del servidor y tal vez el proveedor tiene dificultades para determinar de qué servidor (local o remoto) obtener este identificador único, aunque sea nulo. Es por eso que probablemente pueda emitir cualquier otro tipo de datos con éxito en este escenario, pero no un identificador único. Los tipos de datos que dependen del 'servidor' como UNIQUEIDENTIFIERS y DATETIMEOFFSET le darán el error que está encontrando.

Usar OPENQUERY en lugar del nombre de 4 partes funciona.

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
fuente
¿Qué es exactamente lo que quiere decir con Uniqueidentifiery DateTimeOffsetsiendo dependiente del servidor? ¿Tienes alguna fuente para esto?
krystah
@krystah: desde este enlace ( technet.microsoft.com/en-us/library/ms190215(v=sql.105).aspx ) "El tipo de datos de identificador único almacena valores binarios de 16 bytes que funcionan como identificadores únicos globales (GUID) . Un GUID es un número binario único; ninguna otra computadora en el mundo generará un duplicado de ese valor GUID. El uso principal de un GUID es asignar un identificador que debe ser único en una red que tiene muchas computadoras en muchos sitios. "
Scott Hodgin
Para DateTimeOffSet ( msdn.microsoft.com/en-us/library/bb630289.aspx ) Define una fecha que se combina con una hora del día que tiene conciencia de zona horaria y se basa en un reloj de 24 horas
Scott Hodgin
Estoy 'deduciendo' que, dado que estos dos tipos de datos 'parecen' ser los únicos que dan el error en su escenario y esos tipos de datos son 'dependientes del servidor', esa es la razón de su problema. Si usa OPENQUERY para recuperar sus resultados de su vista y agregar los lanzamientos nulos adicionales, debería funcionar porque el proveedor sabe "dónde" obtener esa información
Scott Hodgin
@krystah y Scott: estos dos tipos de datos no dependen del servidor, al menos no en cómo se describe y se implica. Los GUID dependen de la "arquitectura" en términos de su representación binaria subyacente (ver aquí para más detalles), pero si eso fuera un problema aquí, entonces ningún GUID se transferiría correctamente. Para DATETIMEOFFSET, eso solo sabe de un desplazamiento, no de una zona horaria / TimeZoneID real. Si bien ambos usan información del sistema para generar nuevos valores, aquí no se generan nuevos valores, y si lo fueran, no se generarían en el proveedor.
Solomon Rutzky
0

Solución alternativa: la respuesta aceptada parece indicar que la conversión debe realizarse localmente porque el controlador OLEDB no la admite.

Entonces, creo que una solución simple (al menos en el caso de mi consulta que selecciona un nulo uniqueidentifieren el caso base de un CTE recursivo) es declarar una variable nula:

declare @nullGuid as uniqueidentifier = null;

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

--use
@nullGuid AS [GUID]
xr280xr
fuente