Detecta si algún valor en las columnas NVARCHAR es realmente unicode

14

Heredé algunas bases de datos de SQL Server. Hay una tabla (llamaré "G"), con aproximadamente 86.7 millones de filas y 41 columnas de ancho, de una base de datos de origen (llamaré "Q") en SQL Server 2014 Standard que lleva ETL a una base de datos de destino (llamaré "P") con el mismo nombre de tabla en SQL Server 2008 R2 Standard.

es decir, [Q]. [G] ---> [P]. [G]

EDITAR: 20/03/2017: Algunas personas han preguntado si la tabla de origen es la ÚNICA fuente de la tabla de destino. Sí, es la única fuente. En lo que respecta al ETL, no ocurre ninguna transformación real; efectivamente está destinado a ser una copia 1: 1 de los datos de origen. Por lo tanto, no hay planes para agregar fuentes adicionales a esta tabla de destino.

Un poco más de la mitad de las columnas en [Q]. [G] son ​​VARCHAR (tabla fuente):

  • 13 de las columnas son VARCHAR (80)
  • 9 de las columnas son VARCHAR (30)
  • 2 de las columnas son VARCHAR (8).

Del mismo modo, las mismas columnas en [P]. [G] son ​​NVARCHAR (tabla de destino), con el mismo número de columnas con los mismos anchos. (En otras palabras, la misma longitud, pero NVARCHAR).

  • 13 de las columnas son NVARCHAR (80)
  • 9 de las columnas son NVARCHAR (30)
  • 2 de las columnas son NVARCHAR (8).

Este no es mi diseño.

Me gustaría ALTERAR [P]. [G] (destino) tipos de datos de columnas de NVARCHAR a VARCHAR. Quiero hacerlo de forma segura (sin pérdida de datos por la conversión).

¿Cómo puedo ver los valores de datos en cada columna NVARCHAR en la tabla de destino para confirmar si la columna contiene o no datos Unicode?

Una consulta (¿DMV?) Que puede verificar cada valor (en un bucle?) De cada columna NVARCHAR y decirme si ALGUNO de los valores es genuino, Unicode sería la solución ideal, pero otros métodos son bienvenidos.

John G Hohengarten
fuente
2
Primero, considere su proceso y cómo se utilizan los datos. Los datos en [G]ETL se transfieren a [P]. Si [G]es así varchar, y el proceso ETL es la única forma en que entran los datos [P], a menos que el proceso agregue verdaderos caracteres Unicode, no debería haber ninguno. Si otros procesos agregan o modifican datos [P], debe ser más cuidadoso, solo porque todos los datos actuales pueden ser varchar, no significa que los nvarchardatos no puedan agregarse mañana. Del mismo modo, es posible que lo que sea que esté consumiendo los datos en datos de [P]necesidades nvarchar.
RDFozz

Respuestas:

10

Supongamos que una de sus columnas no contiene datos Unicode. Para verificar que necesitaría leer el valor de la columna para cada fila. A menos que tenga un índice en la columna, con una tabla de almacén de filas necesitará leer cada página de datos de la tabla. Con eso en mente, creo que tiene mucho sentido combinar todas las comprobaciones de columnas en una sola consulta en la tabla. De esa manera, no leerá los datos de la tabla muchas veces y no tendrá que codificar un cursor u otro tipo de bucle.

Para verificar una sola columna, crea que puede hacer esto:

SELECT COLUMN_1
FROM [P].[Q]
WHERE CAST(COLUMN_1 AS VARCHAR(80)) <> CAST(COLUMN_1 AS NVARCHAR(80));

Una conversión de NVARCHARa VARCHARdebería darte el mismo resultado, excepto si hay caracteres Unicode. Los caracteres Unicode se convertirán a ?. Entonces, el código anterior debe manejar los NULLcasos correctamente. Tiene 24 columnas para verificar, por lo que verifica cada columna en una sola consulta utilizando agregados escalares. Una implementación está abajo:

SELECT 
  MAX(CASE WHEN CAST(COLUMN_1 AS VARCHAR(80)) <> CAST(COLUMN_1 AS NVARCHAR(80)) THEN 1 ELSE 0 END) COLUMN_1_RESULT
...
, MAX(CASE WHEN CAST(COLUMN_14 AS VARCHAR(30)) <> CAST(COLUMN_14 AS NVARCHAR(30)) THEN 1 ELSE 0 END) COLUMN_14_RESULT
...
, MAX(CASE WHEN CAST(COLUMN_23 AS VARCHAR(8)) <> CAST(COLUMN_23 AS NVARCHAR(8)) THEN 1 ELSE 0 END) COLUMN_23_RESULT
FROM [P].[Q];

Para cada columna obtendrá un resultado de 1si alguno de sus valores contiene unicode. Un resultado de 0significa que todos los datos se pueden convertir de forma segura.

Recomiendo hacer una copia de la tabla con las nuevas definiciones de columna y copiar sus datos allí. Hará conversiones costosas si lo hace en su lugar, por lo que hacer una copia podría no ser mucho más lento. Tener una copia significa que puede validar fácilmente que todos los datos todavía están allí (una forma es usar la palabra clave EXCEPT ) y puede deshacer la operación muy fácilmente.

Además, tenga en cuenta que es posible que actualmente no tenga datos Unicode, es posible que un ETL futuro cargue Unicode en una columna previamente limpia. Si no hay una verificación para esto en su proceso ETL, debería considerar agregar eso antes de hacer esta conversión.

Joe Obbish
fuente
Si bien la respuesta y discusión de @srutzky fue bastante buena y tenía información útil, Joe me proporcionó lo que pedía mi pregunta: una consulta para decirme si algún valor en las columnas realmente tiene Unicode. Por lo tanto, marqué la respuesta de Joe como la respuesta aceptada. Voté las otras respuestas que también me ayudaron.
John G Hohengarten
@JohnGHohengarten y Joe: Eso está bien. No mencioné la consulta ya que estaba en esta respuesta, así como en la de Scott. Solo diría que no hay necesidad de convertir la NVARCHARcolumna NVARCHARya que ya es de ese tipo. Y no estoy seguro de cómo determinó el carácter no convertible, pero puede convertir la columna VARBINARYpara obtener las secuencias de bytes UTF-16. Y UTF-16 es el orden inverso de bytes, entonces p= 0x7000y luego invierte esos dos bytes para obtener Code Point U+0070. Pero, si la fuente es VARCHAR, entonces no puede ser un carácter Unicode. Algo más está sucediendo. Necesito más información
Solomon Rutzky
@srutzky Agregué el elenco para evitar problemas de precedencia de tipo de datos. Puede ser correcto que no es necesario. Para la otra pregunta, sugerí UNICODE () y SUBSTRING (). ¿Funciona ese enfoque?
Joe Obbish
@JohnGHohengarten y Joe: la precedencia del tipo de datos no debería ser un problema, ya que VARCHARse convertirá implícitamente NVARCHAR, pero podría ser mejor hacerlo CONVERT(NVARCHAR(80), CONVERT(VARCHAR(80), column)) <> column. SUBSTRINGa veces funciona, pero no funciona con caracteres suplementarios cuando se usan intercalaciones que no terminan _SC, y el que usa John no funciona, aunque probablemente no sea un problema aquí. Pero la conversión a VARBINARIO siempre funciona. Y CONVERT(VARCHAR(10), CONVERT(NVARCHAR(10), '›'))no da como resultado ?, por lo que me gustaría ver los bytes. El proceso ETL podría haberlo convertido.
Solomon Rutzky
5

Antes de hacer nada, considere las preguntas planteadas por @RDFozz en un comentario sobre la pregunta, a saber:

  1. ¿Hay alguna otras fuentes además de [Q].[G]poblar esta tabla?

    Si la respuesta es algo fuera de "Estoy 100% seguro de que esta es la única fuente de datos para esta tabla de destino", no realice ningún cambio, independientemente de si los datos actualmente en la tabla se pueden convertir o no sin pérdida de datos.

  2. ¿Hay alguna planes / discusiones relacionadas con la adición de fuentes adicionales para rellenar estos datos en un futuro próximo?

    Y yo añadiría una pregunta relacionada: ¿Ha habido alguna discusión en torno a soportar múltiples idiomas en la tabla de fuente de corriente (es decir [Q].[G]) mediante la conversión es a NVARCHAR?

    Tendrá que preguntar para tener una idea de estas posibilidades. Supongo que actualmente no le han dicho nada que apunte en esta dirección; de lo contrario, no estaría haciendo esta pregunta, pero si se supone que estas preguntas son "no", entonces deben formularse, y se les debe hacer una pregunta. audiencia lo suficientemente amplia como para obtener la respuesta más precisa / completa.

El problema principal aquí no es tanto tener puntos de código Unicode que no se pueden convertir (nunca), sino más bien tener puntos de código que no encajen en una sola página de códigos. Eso es lo bueno de Unicode: puede contener caracteres de TODAS las páginas de códigos. Si realiza la conversión desde NVARCHAR, donde no necesita preocuparse por las páginas de códigos, a VARCHAR, deberá asegurarse de que la Clasificación de la columna de destino esté utilizando la misma página de códigos que la columna de origen. Esto supone tener una sola fuente o múltiples fuentes usando la misma página de códigos (aunque no necesariamente la misma Clasificación). Pero si hay varias fuentes con varias páginas de códigos, entonces puede encontrarse con el siguiente problema:

DECLARE @Reporting TABLE
(
  ID INT IDENTITY(1, 1) PRIMARY KEY,
  SourceSlovak VARCHAR(50) COLLATE Slovak_CI_AS,
  SourceHebrew VARCHAR(50) COLLATE Hebrew_CI_AS,
  Destination NVARCHAR(50) COLLATE Latin1_General_CI_AS,
  DestinationS VARCHAR(50) COLLATE Slovak_CI_AS,
  DestinationH VARCHAR(50) COLLATE Hebrew_CI_AS
);

INSERT INTO @Reporting ([SourceSlovak]) VALUES (0xDE20FA);
INSERT INTO @Reporting ([SourceHebrew]) VALUES (0xE820FA);

UPDATE @Reporting
SET    [Destination] = [SourceSlovak]
WHERE  [SourceSlovak] IS NOT NULL;

UPDATE @Reporting
SET    [Destination] = [SourceHebrew]
WHERE  [SourceHebrew] IS NOT NULL;

SELECT * FROM @Reporting;

UPDATE @Reporting
SET    [DestinationS] = [Destination],
       [DestinationH] = [Destination]

SELECT * FROM @Reporting;

Devoluciones (segundo conjunto de resultados):

ID    SourceSlovak    SourceHebrew    Destination    DestinationS    DestinationH
1     Ţ ú             NULL            Ţ ú            Ţ ú             ? ?
2     NULL            ט ת             ? ?            ט ת             ט ת

Como puede ver, todos esos caracteres se pueden convertir VARCHAR, pero no en la misma VARCHARcolumna.

Use la siguiente consulta para determinar cuál es la página de códigos para cada columna de su tabla de origen:

SELECT OBJECT_NAME(sc.[object_id]) AS [TableName],
       COLLATIONPROPERTY(sc.[collation_name], 'CodePage') AS [CodePage],
       sc.*
FROM   sys.columns sc
WHERE  OBJECT_NAME(sc.[object_id]) = N'source_table_name';

HABIENDO DICHO ESO....

Usted mencionó estar en SQL Server 2008 R2, PERO, no dijo qué edición. Si se encuentra en Enterprise Edition, olvídese de todas estas cosas de conversión (ya que probablemente lo esté haciendo solo para ahorrar espacio) y habilite la compresión de datos:

Implementación de compresión Unicode

Si usa Standard Edition (y ahora parece que usted es then), entonces hay otra posibilidad muy remota: la actualización a SQL Server 2016 ya que SP1 incluye la capacidad de todas las ediciones para usar Compresión de datos (recuerde, dije "posibilidad remota "😉).

Por supuesto, ahora que se acaba de aclarar que solo hay una fuente para los datos, entonces no tiene nada de qué preocuparse, ya que la fuente no puede contener caracteres exclusivos de Unicode o caracteres fuera de su código específico. página. En ese caso, lo único que debe tener en cuenta es usar la misma Clasificación que la columna de origen, o al menos una que use la misma Página de códigos. Es decir, si la columna de origen está usando SQL_Latin1_General_CP1_CI_AS, entonces podría usar Latin1_General_100_CI_ASen el destino.

Una vez que sepa qué colación usar, puede:

  • ALTER TABLE ... ALTER COLUMN ...ser VARCHAR(asegúrese de especificar la corriente NULL/ NOT NULLajuste), lo que requiere un poco de tiempo y una gran cantidad de espacio de registro de transacciones de 87 millones de filas, OR

  • Cree nuevas columnas "ColumnName_tmp" para cada una y complete lentamente UPDATEhaciendo TOP (1000) ... WHERE new_column IS NULL. Una vez que todas las filas están pobladas (¡y validado que todas se copiaron correctamente! Es posible que necesite un activador para manejar ACTUALIZACIONES, si las hay), en una transacción explícita, use sp_renamepara intercambiar los nombres de columna de las columnas "actuales" por " _Old "y luego las nuevas columnas" _tmp "para simplemente eliminar el" _tmp "de los nombres. Luego llame sp_reconfigurea la tabla para invalidar cualquier plan en caché que haga referencia a la tabla, y si hay Vistas que hagan referencia a la tabla, deberá llamar sp_refreshview(o algo así). Una vez que haya validado la aplicación y ETL esté funcionando correctamente con ella, puede soltar las columnas.

Solomon Rutzky
fuente
Ejecuté la consulta de CodePage que proporcionó tanto en origen como en destino, y CodePage es 1252 y collation_name es SQL_Latin1_General_CP1_CI_AS en AMBOS origen y destino.
John G Hohengarten
@JohnGHohengarten Acabo de actualizar nuevamente, en la parte inferior. Para ser fácil, puede mantener la misma intercalación, aunque Latin1_General_100_CI_ASes mucho mejor que la que está utilizando. Significa fácilmente que el comportamiento de clasificación y comparación será el mismo entre ellos, incluso si no es tan bueno como la nueva Colación que acabo de mencionar.
Solomon Rutzky
4

Tengo algo de experiencia con esto cuando tenía un trabajo real. Como en ese momento quería preservar los datos base, y también tenía que dar cuenta de los nuevos datos que posiblemente podrían tener caracteres que se perderían en la confusión, elegí una columna computada no persistente.

Aquí hay un ejemplo rápido usando una copia de la base de datos Super User del volcado de datos SO .

Podemos ver de inmediato que hay DisplayNames con caracteres Unicode:

Nueces

¡Agreguemos una columna calculada para calcular cuántos! La columna DisplayName es NVARCHAR(40).

USE SUPERUSER

ALTER TABLE dbo.Users
ADD DisplayNameStandard AS CONVERT(VARCHAR(40), DisplayName)

SELECT COUNT_BIG(*)
FROM dbo.Users AS u
WHERE u.DisplayName <> u.DisplayNameStandard

El recuento devuelve ~ 3000 filas

Nueces

Sin embargo, el plan de ejecución es un poco pesado. La consulta finaliza rápidamente, pero este conjunto de datos no es terriblemente grande.

Nueces

Dado que las columnas calculadas no necesitan persistir para agregar un índice, podemos hacer una de estas:

CREATE UNIQUE NONCLUSTERED INDEX ix_helper
ON dbo.Users(DisplayName, DisplayNameStandard, Id)

Lo que nos da un plan un poco más ordenado:

Nueces

Entiendo si esto no es la respuesta, ya que implica cambios arquitectónicos, pero teniendo en cuenta el tamaño de los datos, probablemente esté buscando agregar índices para hacer frente a las consultas que de todos modos se unen a la tabla.

¡Espero que esto ayude!

Erik Darling
fuente
1

Usando el ejemplo en Cómo verificar si un campo contiene datos Unicode , puede leer los datos en cada columna y hacer lo siguiente CASTy verificar:

--Test 1:
DECLARE @text NVARCHAR(100)
SET @text = N'This is non-Unicode text, in Unicode'
IF CAST(@text AS VARCHAR(MAX)) <> @text
PRINT 'Contains Unicode characters'
ELSE
PRINT 'No Unicode characters'
GO

--Test 2:
DECLARE @text NVARCHAR(100)
SET @text = N'This is Unicode (字) text, in Unicode'
IF CAST(@text AS VARCHAR(MAX)) <> @text
PRINT 'Contains Unicode characters'
ELSE
PRINT 'No Unicode characters'

GO
Scott Hodgin
fuente