Détectez si les valeurs des colonnes NVARCHAR sont réellement unicode

14

J'ai hérité de certaines bases de données SQL Server. Il y a une table (je vais appeler "G"), avec environ 86,7 millions de lignes et 41 colonnes de large, à partir d'une base de données source (je vais appeler "Q") sur SQL Server 2014 Standard qui obtient ETL pour une base de données cible (je vais appeler "P") avec le même nom de table sur SQL Server 2008 R2 Standard.

c'est-à-dire [Q]. [G] ---> [P]. [G]

EDIT: 3/20/2017: Certaines personnes ont demandé si la table source est la SEULE source de la table cible. Oui, c'est la seule source. En ce qui concerne l'ETL, aucune transformation réelle ne se produit; il s'agit en fait d'une copie 1: 1 des données source. Par conséquent, il n'est pas prévu d'ajouter des sources supplémentaires à cette table cible.

Un peu plus de la moitié des colonnes de [Q]. [G] sont VARCHAR (tableau source):

  • 13 des colonnes sont VARCHAR (80)
  • 9 des colonnes sont VARCHAR (30)
  • 2 des colonnes sont VARCHAR (8).

De même, les mêmes colonnes dans [P]. [G] sont NVARCHAR (table cible), avec le même nombre de colonnes avec les mêmes largeurs. (En d'autres termes, même longueur, mais NVARCHAR).

  • 13 des colonnes sont NVARCHAR (80)
  • 9 des colonnes sont NVARCHAR (30)
  • 2 des colonnes sont NVARCHAR (8).

Ce n'est pas ma conception.

Je voudrais ALTER [P]. [G] (cible) les types de données des colonnes de NVARCHAR à VARCHAR. Je veux le faire en toute sécurité (sans perte de données de conversion).

Comment puis-je consulter les valeurs de données dans chaque colonne NVARCHAR de la table cible pour confirmer si la colonne contient réellement des données Unicode?

Une requête (DMV?) Qui peut vérifier chaque valeur (dans une boucle?) De chaque colonne NVARCHAR et me dire si TOUTES les valeurs est authentique Unicode serait la solution idéale, mais d'autres méthodes sont les bienvenues.

John G Hohengarten
la source
2
Tout d'abord, réfléchissez à votre processus et à la façon dont les données sont utilisées. Les données en [G]sont ETLed sur [P]. Si [G]c'est le cas varchar, et que le processus ETL est le seul moyen par lequel les données entrent [P], alors à moins que le processus ajoute de vrais caractères Unicode, il ne devrait pas y en avoir. Si d'autres processus ajoutent ou modifient des données [P], vous devez être plus prudent - ce n'est pas parce que toutes les données actuelles peuvent l'être varcharque les nvarchardonnées ne pourront pas être ajoutées demain. De même, il est possible que tout ce qui consomme les données en ait [P]besoin nvarchar.
RDFozz

Réponses:

10

Supposons qu'une de vos colonnes ne contienne aucune donnée Unicode. Pour vérifier que vous devez lire la valeur de colonne pour chaque ligne. Sauf si vous avez un index sur la colonne, avec une table rowstore, vous devrez lire chaque page de données de la table. Dans cet esprit, je pense qu'il est très logique de combiner toutes les vérifications de colonne en une seule requête sur la table. De cette façon, vous ne lirez pas les données de la table plusieurs fois et vous n'aurez pas à coder un curseur ou un autre type de boucle.

Pour vérifier une seule colonne, pensez que vous pouvez simplement faire ceci:

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

Un cast de NVARCHARà VARCHARdevrait vous donner le même résultat sauf s'il y a des caractères Unicode. Les caractères Unicode seront convertis en ?. Le code ci-dessus doit donc gérer NULLcorrectement les cas. Vous avez 24 colonnes à vérifier, vous vérifiez donc chaque colonne dans une seule requête en utilisant des agrégats scalaires. Une implémentation est ci-dessous:

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

Pour chaque colonne, vous obtiendrez un résultat 1si l'une de ses valeurs contient unicode. Le résultat 0signifie que toutes les données peuvent être converties en toute sécurité.

Je recommande fortement de faire une copie du tableau avec les nouvelles définitions de colonne et d'y copier vos données. Vous effectuerez des conversions coûteuses si vous le faites sur place, donc faire une copie pourrait ne pas être beaucoup plus lent. Avoir une copie signifie que vous pouvez facilement valider que toutes les données sont toujours là (une façon consiste à utiliser le mot-clé EXCEPT ) et vous pouvez annuler l'opération très facilement.

En outre, sachez que vous ne disposez peut-être pas de données Unicode actuellement, il est possible qu'un futur ETL puisse charger Unicode dans une colonne précédemment propre. S'il n'y a pas de vérification pour cela dans votre processus ETL, vous devriez envisager d'ajouter cela avant de faire cette conversion.

Joe Obbish
la source
Alors que la réponse et la discussion de @srutzky étaient assez bonnes et contenaient des informations utiles, Joe m'a fourni ce que ma question demandait: une requête pour me dire si des valeurs dans les colonnes ont réellement Unicode. J'ai donc marqué la réponse de Joe comme étant la réponse acceptée. J'ai voté pour les autres réponses qui m'ont également aidé.
John G Hohengarten du
@JohnGHohengarten et Joe: C'est bien. Je n'ai pas mentionné la requête car elle figurait dans cette réponse ainsi que dans celle de Scott. Je dirais simplement qu'il n'est pas nécessaire de convertir la NVARCHARcolonne NVARCHARcar elle est déjà de ce type. Et vous ne savez pas comment vous avez déterminé le caractère non convertible, mais vous pouvez convertir la colonne VARBINARYpour obtenir les séquences d'octets UTF-16. Et UTF-16 est l'ordre des octets inversés, donc p= 0x7000, puis vous inversez ces deux octets pour obtenir le point de code U+0070. Mais, si la source est VARCHAR, il ne peut pas s'agir d'un caractère Unicode. Il se passe autre chose. Besoin de plus d'informations.
Solomon Rutzky
@srutzky J'ai ajouté la distribution pour éviter les problèmes de priorité des types de données. Vous avez peut-être raison de dire que ce n'est pas nécessaire. Pour l'autre question, j'ai suggéré UNICODE () et SUBSTRING (). Cette approche fonctionne-t-elle?
Joe Obbish
@JohnGHohengarten et Joe: la priorité des types de données ne devrait pas être un problème, comme cela VARCHARsera implicitement converti en NVARCHAR, mais il pourrait être préférable de le faire CONVERT(NVARCHAR(80), CONVERT(VARCHAR(80), column)) <> column. SUBSTRINGfonctionne parfois, mais cela ne fonctionne pas avec les caractères supplémentaires lors de l'utilisation de classements qui ne se terminent pas _SC, et celui que John utilise ne fonctionne pas, mais ce n'est probablement pas un problème ici. Mais la conversion en VARBINARY fonctionne toujours. Et CONVERT(VARCHAR(10), CONVERT(NVARCHAR(10), '›'))ne se traduit pas par ?, donc je voudrais voir les octets. Le processus ETL a pu le convertir.
Solomon Rutzky
5

Avant de faire quoi que ce soit, veuillez considérer les questions posées par @RDFozz dans un commentaire sur la question, à savoir:

  1. Y a-t-il d' autres sources en plus de [Q].[G]remplir ce tableau?

    Si la réponse est autre que "Je suis sûr à 100% qu'il s'agit de la seule source de données pour cette table de destination", n'apportez aucune modification, que les données actuellement dans la table puissent ou non être converties sans perte de données.

  2. Y a-t-il des plans / discussions concernant l'ajout de sources supplémentaires pour alimenter ces données dans un avenir proche?

    Et je voudrais ajouter une question connexe: Y at - il eu des discussions autour de plusieurs langues dans le soutien de la table source de courant (ie [Q].[G]) en convertissant ce à NVARCHAR?

    Vous devrez demander autour de vous pour avoir une idée de ces possibilités. Je suppose qu'on ne vous a actuellement rien dit qui pourrait aller dans cette direction, sinon vous ne poseriez pas cette question, mais si ces questions ont été supposées être "non", alors elles doivent être posées et un public suffisamment large pour obtenir la réponse la plus précise / complète.

Le problème principal ici n'est pas tant d'avoir des points de code Unicode qui ne peuvent pas convertir (jamais), mais plus encore d'avoir des points de code qui ne tiennent pas tous sur une seule page de code. C'est la bonne chose à propos d'Unicode: il peut contenir des caractères de TOUTES les pages de codes. Si vous effectuez une conversion de NVARCHAR- où vous n'avez pas à vous soucier des pages de codes - vers VARCHAR, vous devrez vous assurer que le classement de la colonne de destination utilise la même page de codes que la colonne source. Cela suppose d'avoir une ou plusieurs sources utilisant la même page de codes (pas nécessairement le même classement, cependant). Mais s'il existe plusieurs sources avec plusieurs pages de codes, vous pouvez potentiellement rencontrer le problème suivant:

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;

Renvoie (2e jeu de résultats):

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

Comme vous pouvez le voir, tous ces caractères peuvent être convertis en VARCHAR, mais pas dans la même VARCHARcolonne.

Utilisez la requête suivante pour déterminer la page de codes pour chaque colonne de votre table source:

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

CELA ÉTANT DIT....

Vous avez mentionné être sur SQL Server 2008 R2, MAIS, vous n'avez pas dit quelle édition. SI vous êtes sur Enterprise Edition, alors oubliez tout ce truc de conversion (puisque vous le faites probablement juste pour économiser de l'espace), et activez la compression des données:

Implémentation de la compression Unicode

Si vous utilisez Standard Edition (et il semble maintenant que vous l'êtes), il existe une autre possibilité à très long terme: la mise à niveau vers SQL Server 2016 puisque SP1 inclut la possibilité pour toutes les éditions d'utiliser la compression de données (rappelez-vous, j'ai dit "à long terme" "😉).

Bien sûr, maintenant qu'il vient d'être clarifié qu'il n'y a qu'une seule source pour les données, alors vous n'avez rien à craindre car la source ne peut pas contenir de caractères Unicode uniquement, ou des caractères en dehors de son code spécifique page. Dans ce cas, la seule chose à laquelle vous devez faire attention est d'utiliser le même classement que la colonne source, ou au moins un qui utilise la même page de codes. Cela signifie que si la colonne source utilise SQL_Latin1_General_CP1_CI_AS, vous pouvez utiliser Latin1_General_100_CI_ASà la destination.

Une fois que vous savez quel classement utiliser, vous pouvez soit:

  • ALTER TABLE ... ALTER COLUMN ...être VARCHAR(assurez-vous de spécifier le paramètre NULL/ actuel NOT NULL), ce qui nécessite un peu de temps et beaucoup d'espace de journal des transactions pour 87 millions de lignes, OU

  • Créez de nouvelles colonnes "ColumnName_tmp" pour chacune et remplissez lentement via UPDATEdo TOP (1000) ... WHERE new_column IS NULL. Une fois que toutes les lignes sont remplies (et validées qu'elles ont toutes été copiées correctement! Vous pourriez avoir besoin d'un déclencheur pour gérer les MISES À JOUR, s'il y en a), dans une transaction explicite, utilisez sp_renamepour permuter les noms des colonnes des colonnes "actuelles" à " _Old "puis les nouvelles colonnes" _tmp "pour supprimer simplement" _tmp "des noms. Appelez ensuite sp_reconfigurela table pour invalider tous les plans mis en cache référençant la table, et s'il existe des vues référençant la table, vous devrez appeler sp_refreshview(ou quelque chose comme ça). Une fois que vous avez validé l'application et qu'ETL fonctionne correctement, vous pouvez supprimer les colonnes.

Solomon Rutzky
la source
J'ai exécuté la requête CodePage que vous avez fournie à la fois sur la source et la cible, et le CodePage est 1252 et collation_name est SQL_Latin1_General_CP1_CI_AS sur les DEUX sources ET la cible.
John G Hohengarten du
@JohnGHohengarten Je viens de mettre à jour à nouveau, en bas. Pour être facile, vous pouvez conserver le même classement, même s'il Latin1_General_100_CI_ASest bien meilleur que celui que vous utilisez. Cela signifie que le comportement de tri et de comparaison sera le même entre eux, même s'il n'est pas aussi bon que le nouveau classement que je viens de mentionner.
Solomon Rutzky
4

J'ai une certaine expérience avec cela de l'arrière quand j'avais un vrai travail. Comme à l'époque je voulais conserver les données de base, et que je devais également tenir compte des nouvelles données qui pourraient éventuellement avoir des caractères qui seraient perdus dans le shuffle, je suis allé avec une colonne calculée non persistante.

Voici un exemple rapide utilisant une copie de la base de données Super User du vidage de données SO .

Nous pouvons voir dès le départ qu'il existe des DisplayNames avec des caractères Unicode:

Des noisettes

Ajoutons donc une colonne calculée pour déterminer combien! La colonne DisplayName est 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

Le compte renvoie ~ 3000 lignes

Des noisettes

Cependant, le plan d'exécution est un peu compliqué. La requête se termine rapidement, mais cet ensemble de données n'est pas terriblement volumineux.

Des noisettes

Comme les colonnes calculées n'ont pas besoin d'être persistées pour ajouter un index, nous pouvons effectuer l'une des actions suivantes:

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

Ce qui nous donne un plan un peu plus ordonné:

Des noisettes

Je comprends que ce n'est pas la réponse, car cela implique des modifications architecturales, mais compte tenu de la taille des données, vous envisagez probablement d'ajouter des index pour faire face aux requêtes qui se joignent de toute façon à la table.

J'espère que cela t'aides!

Erik Darling
la source
1

En utilisant l'exemple dans Comment vérifier si un champ contient des données Unicode , vous pouvez lire les données dans chaque colonne et faire le CASTet vérifier ci-dessous:

--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
la source