Detectar se algum valor nas colunas NVARCHAR é realmente unicode

14

Eu herdei alguns bancos de dados do SQL Server. Há uma tabela (chamarei "G"), com cerca de 86,7 milhões de linhas e 41 colunas de largura, de um banco de dados de origem (chamarei "Q") no SQL Server 2014 Standard que obtém o ETL para um banco de dados de destino (chamarei "P") com o mesmo nome de tabela no SQL Server 2008 R2 Standard.

ou seja, [Q]. [G] ---> [P]. [G]

EDIT: 20/03/2017: Algumas pessoas perguntaram se a tabela de origem é a ÚNICA fonte da tabela de destino. Sim, é a única fonte. No que diz respeito à ETL, não há nenhuma transformação real acontecendo; efetivamente pretende ser uma cópia 1: 1 dos dados de origem. Portanto, não há planos para adicionar fontes adicionais a esta tabela de destino.

Um pouco mais da metade das colunas em [Q]. [G] são VARCHAR (tabela de origem):

  • 13 das colunas são VARCHAR (80)
  • 9 das colunas são VARCHAR (30)
  • 2 das colunas são VARCHAR (8).

Da mesma forma, as mesmas colunas em [P]. [G] são NVARCHAR (tabela de destino), com o mesmo número de colunas com as mesmas larguras. (Em outras palavras, mesmo comprimento, mas NVARCHAR).

  • 13 das colunas são NVARCHAR (80)
  • 9 das colunas são NVARCHAR (30)
  • 2 das colunas são NVARCHAR (8).

Este não é o meu design.

Gostaria de alterar os tipos de dados das colunas [P]. [G] (destino) de NVARCHAR para VARCHAR. Quero fazê-lo com segurança (sem perda de dados da conversão).

Como posso observar os valores dos dados em cada coluna NVARCHAR na tabela de destino para confirmar se a coluna realmente contém ou não dados Unicode?

Uma consulta (DMVs?) Que pode verificar cada valor (em um loop?) De cada coluna NVARCHAR e me informar se QUALQUER valor é genuíno, o Unicode seria a solução ideal, mas outros métodos são bem-vindos.

John G Hohengarten
fonte
2
Primeiro, considere seu processo e como os dados são usados. Os dados [G]são ETLed para [P]. Se [G]for varchar, e o processo ETL é a única maneira pela qual os dados entram [P], a menos que o processo adicione caracteres Unicode verdadeiros, não deve haver nenhum. Se outros processos adicionam ou modificam dados [P], você precisa ter mais cuidado - apenas porque todos os dados atuais podem estar varchar, não significa que os nvarchardados não possam ser adicionados amanhã. Da mesma forma, é possível que o que estiver consumindo os dados [P]precise de nvarchardados.
RDFozz

Respostas:

10

Suponha que uma de suas colunas não contenha dados unicode. Para verificar se você precisaria ler o valor da coluna para cada linha. A menos que você tenha um índice na coluna, com uma tabela rowstore, você precisará ler todas as páginas de dados da tabela. Com isso em mente, acho que faz muito sentido combinar todas as verificações de coluna em uma única consulta na tabela. Dessa forma, você não estará lendo os dados da tabela muitas vezes e não precisará codificar um cursor ou algum outro tipo de loop.

Para verificar uma única coluna, acredite que você pode fazer isso:

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

Uma conversão de NVARCHARpara VARCHARdeve fornecer o mesmo resultado, exceto se houver caracteres unicode. Caracteres Unicode serão convertidos para ?. Portanto, o código acima deve tratar os NULLcasos corretamente. Você tem 24 colunas para verificar, portanto, verifique cada coluna em uma única consulta usando agregados escalares. Uma implementação está abaixo:

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 coluna, você obterá um resultado 1se algum de seus valores contiver unicode. Um resultado 0significa que todos os dados podem ser convertidos com segurança.

Eu recomendo fazer uma cópia da tabela com as novas definições de coluna e copiar seus dados lá. Você estará fazendo conversões caras, se fizer isso no local, para fazer uma cópia não ser muito mais lenta. Ter uma cópia significa que você pode facilmente validar que todos os dados ainda estão lá (uma maneira é usar a palavra-chave EXCEPT ) e você pode desfazer a operação com muita facilidade.

Além disso, esteja ciente de que você pode não ter dados unicode atualmente. É possível que um ETL futuro possa carregar o unicode em uma coluna limpa anteriormente. Se não houver uma verificação para isso em seu processo ETL, considere adicionar isso antes de fazer essa conversão.

Joe Obbish
fonte
Embora a resposta e a discussão de @srutzky tenham sido muito boas e tenham informações úteis, Joe me forneceu o que minha pergunta estava pedindo: uma consulta para me informar se algum valor nas colunas realmente tem Unicode. Portanto, marquei a resposta de Joe como a resposta aceita. Votei nas demais respostas que também me ajudaram.
John G Hohengarten /
@JohnGHohengarten e Joe: Tudo bem. Não mencionei a consulta, pois estava nessa resposta e também na de Scott. Eu diria apenas que não há necessidade de converter a NVARCHARcoluna, NVARCHARpois ela já é desse tipo. E não sabe como você determinou o caractere inconversível, mas é possível converter a coluna VARBINARYpara obter as seqüências de bytes UTF-16. E UTF-16 é a ordem inversa de bytes, então p= 0x7000e então você inverte esses dois bytes para obter o Ponto de Código U+0070. Mas, se a fonte for VARCHAR, não poderá ser um caractere Unicode. Algo mais está acontecendo. Precisa de mais informações.
Solomon Rutzky 21/03/19
@srutzky Adicionei o elenco para evitar problemas de precedência do tipo de dados. Você pode estar certo de que não é necessário. Para a outra pergunta, sugeri UNICODE () e SUBSTRING (). Essa abordagem funciona?
precisa saber é o seguinte
@JohnGHohengarten e Joe: a precedência do tipo de dados não deve ser um problema, pois VARCHARserá implicitamente convertido em NVARCHAR, mas pode ser melhor CONVERT(NVARCHAR(80), CONVERT(VARCHAR(80), column)) <> column. SUBSTRINGàs vezes funciona, mas não funciona com caracteres suplementares ao usar agrupamentos que não terminam _SCe o que John está usando não, embora provavelmente não seja um problema aqui. Mas converter para VARBINARY sempre funciona. E CONVERT(VARCHAR(10), CONVERT(NVARCHAR(10), '›'))não resulta ?, então eu gostaria de ver os bytes. O processo ETL pode ter convertido.
Solomon Rutzky
5

Antes de fazer qualquer coisa, considere as perguntas feitas pelo @RDFozz em um comentário sobre a pergunta, a saber:

  1. quaisquer outras fontes além de [Q].[G]preencher esta tabela?

    Se a resposta estiver fora de "Estou 100% certo de que esta é a única fonte de dados para esta tabela de destino", não faça alterações, independentemente de os dados atualmente na tabela poderem ser convertidos ou não sem perda de dados.

  2. Existem quaisquer planos / discussões relacionadas com a adição de fontes adicionais para preencher esses dados no futuro próximo?

    E eu acrescentaria uma pergunta relacionada: houve alguma discussão sobre o suporte a vários idiomas na tabela de origem atual (ie [Q].[G]) convertendo -a para NVARCHAR?

    Você precisará perguntar ao redor para ter uma idéia dessas possibilidades. Presumo que você não tenha recebido nenhuma informação que apontaria nessa direção, caso contrário, você não faria essa pergunta, mas se essas perguntas forem consideradas "não", elas deverão ser feitas e solicitadas a um público amplo o suficiente para obter a resposta mais precisa / completa.

O principal problema aqui não é tanto ter pontos de código Unicode que não podem ser convertidos (nunca), mas ainda mais ter pontos de código que nem todos se encaixam em uma única página de código. Essa é a coisa legal do Unicode: ele pode conter caracteres de TODAS as páginas de código. Se você converter de NVARCHAR- para onde não precisa se preocupar com páginas de código - para VARCHAR, precisará garantir que o agrupamento da coluna de destino esteja usando a mesma página de código que a coluna de origem. Isso pressupõe ter uma fonte ou várias fontes usando a mesma página de código (embora não necessariamente o mesmo agrupamento). Mas se houver várias fontes com várias páginas de código, você poderá executar o seguinte 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;

Retorna (segundo conjunto de resultados):

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

Como você pode ver, todos esses caracteres podem ser convertidos em VARCHAR, mas não na mesma VARCHARcoluna.

Use a seguinte consulta para determinar qual é a página de código para cada coluna da sua tabela de origem:

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

QUE ESTÁ DISSE ....

Você mencionou estar no SQL Server 2008 R2, MAS, não disse qual edição. Se você estiver no Enterprise Edition, esqueça todas essas conversões (já que provavelmente está fazendo isso apenas para economizar espaço) e ative a compactação de dados:

Implementação de compactação Unicode

Se você estiver usando o Standard Edition (e agora parece que você é 😞), há outra possibilidade de longo prazo: atualizar para o SQL Server 2016, já que o SP1 inclui a capacidade de todas as edições usarem a compactação de dados (lembre-se, eu disse "longo prazo "😉).

Obviamente, agora que acabou de ser esclarecido que há apenas uma fonte para os dados, você não precisa se preocupar, pois a fonte não pode conter caracteres somente Unicode ou caracteres fora do código específico. página. Nesse caso, a única coisa que você deve ter em mente é usar o mesmo agrupamento da coluna de origem ou pelo menos um que esteja usando a mesma página de código. Ou seja, se a coluna de origem estiver usando SQL_Latin1_General_CP1_CI_AS, você poderá usar Latin1_General_100_CI_ASno destino.

Depois de saber qual agrupamento usar, você pode:

  • ALTER TABLE ... ALTER COLUMN ...para ser VARCHAR(não se esqueça de especificar a corrente NULL/ NOT NULLconfiguração), que requer um pouco de tempo e um monte de espaço de log de transação para 87 milhões de linhas, ou

  • Crie novas colunas "ColumnName_tmp" para cada uma e preencha lentamente UPDATEfazendo TOP (1000) ... WHERE new_column IS NULL. Depois que todas as linhas forem preenchidas (e validadas, todas copiadas corretamente! Você pode precisar de um gatilho para manipular UPDATEs, se houver), em uma transação explícita, use sp_renamepara trocar os nomes das colunas "atuais" a serem " _Old "e, em seguida, as novas colunas" _tmp "para simplesmente remover o" _tmp "dos nomes. Em seguida, chame sp_reconfigurea tabela para invalidar quaisquer planos em cache que façam referência à tabela e, se houver Visualizações que façam referência à tabela, você precisará chamar sp_refreshview(ou algo assim). Depois de validar o aplicativo e o ETL estiver funcionando corretamente, você poderá soltar as colunas.

Solomon Rutzky
fonte
Executei a consulta do CodePage que você forneceu na origem e no destino, e o CodePage é 1252 e collation_name é SQL_Latin1_General_CP1_CI_AS em AMBOS origem e destino.
John G Hohengarten
@JohnGHohengarten Acabei de atualizar novamente, na parte inferior. Para ser fácil, você pode manter o mesmo agrupamento, mesmo que Latin1_General_100_CI_ASseja muito melhor que o que você está usando. Fácil significa que o comportamento de classificação e comparação será o mesmo entre eles, mesmo que não seja tão bom quanto o Collation mais recente que acabei de mencionar.
Solomon Rutzky 20/03/19
4

Tenho alguma experiência nisso quando tinha um emprego de verdade. Como na época eu queria preservar os dados de base, e também tinha que dar conta de novos dados que poderiam ter caracteres que seriam perdidos no shuffle, fui com uma coluna computada não persistente.

Aqui está um exemplo rápido usando uma cópia do banco de dados Superusuário do despejo de dados do SO .

Podemos ver logo de cara que existem DisplayNames com caracteres Unicode:

Nozes

Então, vamos adicionar uma coluna computada para descobrir quantas! A coluna DisplayName é 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

A contagem retorna ~ 3000 linhas

Nozes

O plano de execução é um pouco complicado, no entanto. A consulta termina rapidamente, mas esse conjunto de dados não é muito grande.

Nozes

Como as colunas computadas não precisam ser persistidas para adicionar um índice, podemos fazer um destes:

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

O que nos dá um plano um pouco mais organizado:

Nozes

Entendo que essa não é a resposta, pois envolve alterações arquiteturais, mas considerando o tamanho dos dados, você provavelmente está adicionando índices para lidar com consultas que se auto-associam à tabela de qualquer maneira.

Espero que isto ajude!

Erik Darling
fonte
1

Usando o exemplo em Como verificar se um campo contém dados unicode , você pode ler os dados em cada coluna e fazer CASToe verifique abaixo:

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