web-dev-qa-db-fra.com

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

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.

J'aimerais 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 lors de la conversion).

Comment puis-je consulter les valeurs des 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 sont authentiques Unicode serait la solution idéale, mais d'autres méthodes sont les bienvenues.

14
John G Hohengarten

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 transtypage de NVARCHAR vers VARCHAR devrait 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 correctement les cas NULL. 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 de 1 si l'une de ses valeurs contient unicode. Le résultat de 0 signifie 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 ferez 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é SAUF ) 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.

10
Joe Obbish

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

  1. Existe-t-il d'autres autres sources que [Q].[G] Dans ce tableau?

    Si la réponse est quelque chose en dehors de "Je suis certain à 100% qu'il s'agit de la seule source de données pour cette table de destination", alors ne faites pas toutes les modifications, que les données actuellement dans la table puissent être converties ou non sans perte de données.

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

    Et j'ajouterais une question connexe: y a-t-il eu des discussions sur la prise en charge de plusieurs langues dans la table source actuelle (c'est-à-dire [Q].[G]) En la convertissant à 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 que ne peut pas convertir (jamais), mais plus encore d'avoir des points de code qui ne seront pas tous tenir sur une seule page de codes. C'est la bonne chose à propos d'Unicode: il peut contenir des caractères de TOUTES les pages de codes. Si vous convertissez de NVARCHAR - où vous n'avez pas à vous soucier des pages de codes - en VARCHAR, vous devrez vous assurer que le classement de la colonne de destination utilise le même code page comme colonne source. Cela suppose d'avoir soit une source, soit plusieurs sources utilisant la même page de code (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 se convertir en VARCHAR, mais pas dans la même colonne VARCHAR.

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 de données:

Implémentation de la compression Unicode

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

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 celui 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 ... Pour être VARCHAR (assurez-vous de spécifier le paramètre actuel NULL/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 UPDATE faisant 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, le cas échéant), dans une transaction explicite, utilisez sp_rename Pour permuter les noms de colonne du " les "colonnes actuelles" doivent être "_Old", puis les nouvelles colonnes "_tmp" pour supprimer simplement "_tmp" des noms. Ensuite, appelez sp_reconfigure Sur la table pour invalider tous les plans mis en cache référençant la table, et s'il y a 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 avec elle, vous pouvez supprimer les colonnes.

5
Solomon Rutzky

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 SO data dump .

Nous pouvons voir d'emblée qu'il existe des DisplayNames avec des caractères Unicode:

Nuts

Ajoutons donc une colonne calculée pour savoir 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

Nuts

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

Nuts

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é:

Nuts

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!

4
Erik Darling

En utilisant l'exemple de Comment vérifier si un champ contient des données Unicode , vous pouvez lire les données dans chaque colonne et faire le CAST et 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
1
Scott Hodgin