Rileva se i valori nelle colonne NVARCHAR sono effettivamente unicode


14

Ho ereditato alcuni database di SQL Server. Esiste una tabella (chiamerò "G"), con circa 86,7 milioni di righe e 41 colonne di larghezza, da un database di origine (chiamerò "Q") su SQL Server 2014 Standard che ottiene ETL su un database di destinazione (chiamerò "P") con lo stesso nome di tabella su SQL Server 2008 R2 Standard.

cioè [Q]. [G] ---> [P]. [G]

EDIT: 20/03/2017: Alcune persone hanno chiesto se la tabella di origine è SOLO la fonte per la tabella di destinazione. Sì, è l'unica fonte. Per quanto riguarda l'ETL, non si sta verificando alcuna vera trasformazione; è effettivamente inteso come una copia 1: 1 dei dati di origine. Pertanto, non ci sono piani per aggiungere ulteriori fonti a questa tabella di destinazione.

Poco più della metà delle colonne in [Q]. [G] sono VARCHAR (tabella di origine):

  • 13 delle colonne sono VARCHAR (80)
  • 9 delle colonne sono VARCHAR (30)
  • 2 delle colonne sono VARCHAR (8).

Allo stesso modo, le stesse colonne in [P]. [G] sono NVARCHAR (tabella di destinazione), con lo stesso numero di colonne con le stesse larghezze. (In altre parole, stessa lunghezza, ma NVARCHAR).

  • 13 delle colonne sono NVARCHAR (80)
  • 9 delle colonne sono NVARCHAR (30)
  • 2 delle colonne sono NVARCHAR (8).

Questo non è il mio disegno.

Vorrei ALTER [P]. [G] (target) colonne tipi di dati da NVARCHAR a VARCHAR. Voglio farlo in modo sicuro (senza perdita di dati dalla conversione).

Come posso esaminare i valori dei dati in ciascuna colonna NVARCHAR nella tabella di destinazione per confermare se la colonna contiene effettivamente dati Unicode?

Una query (DMV?) Che può controllare ogni valore (in un ciclo?) Di ogni colonna NVARCHAR e dirmi se QUALUNQUE dei valori è un vero Unicode sarebbe la soluzione ideale, ma altri metodi sono i benvenuti.


2
Innanzitutto, considera il tuo processo e come vengono utilizzati i dati. I dati in [G]sono ETLed a [P]. Se lo [G]è varchar, e il processo ETL è l'unico modo in cui entrano i dati [P], a meno che il processo non aggiunga caratteri Unicode veri, non dovrebbe essercene alcuno. Se altri processi aggiungono o modificano dati [P], devi stare più attento - solo perché tutti i dati attuali possono essere varcharnon significa che i nvarchardati non possano essere aggiunti domani. Allo stesso modo, è possibile che qualunque cosa stia consumando i dati nei dati [P]necessari nvarchar.
RDFozz,

Risposte:


10

Supponiamo che una delle tue colonne non contenga dati unicode. Per verificare che è necessario leggere il valore della colonna per ogni riga. A meno che non si disponga di un indice sulla colonna, con una tabella di archivio righe sarà necessario leggere tutte le pagine di dati dalla tabella. Con questo in mente penso che abbia molto senso combinare tutti i controlli di colonna in una singola query contro la tabella. In questo modo non leggerai i dati della tabella molte volte e non dovrai codificare un cursore o qualche altro tipo di loop.

Per controllare una singola colonna, credi che puoi semplicemente fare questo:

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

Un cast da NVARCHARa VARCHARdovrebbe darti lo stesso risultato tranne se ci sono personaggi unicode. I caratteri Unicode verranno convertiti in ?. Quindi il codice sopra dovrebbe gestire i NULLcasi correttamente. Hai 24 colonne da controllare, quindi controlla ogni colonna in una singola query utilizzando aggregati scalari. Una implementazione è di seguito:

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

Per ogni colonna otterrai un risultato 1se uno qualsiasi dei suoi valori contiene unicode. Un risultato di 0significa che tutti i dati possono essere convertiti in modo sicuro.

Consiglio vivamente di fare una copia della tabella con le nuove definizioni di colonna e di copiarvi i dati. Farai conversioni costose se lo fai sul posto, quindi fare una copia potrebbe non essere molto più lento. Avere una copia significa che puoi facilmente convalidare che tutti i dati sono ancora lì (un modo è usare la parola chiave EXCEPT ) e puoi annullare l'operazione molto facilmente.

Inoltre, tieni presente che al momento potresti non disporre di dati Unicode, è possibile che un ETL futuro possa caricare Unicode in una colonna precedentemente pulita. Se non è presente un controllo per questo nel processo ETL, è consigliabile aggiungerlo prima di eseguire questa conversione.


Mentre la risposta e la discussione di @srutzky erano abbastanza buone e avevano informazioni utili, Joe mi ha fornito ciò che la mia domanda mi chiedeva: una query per dirmi se i valori nelle colonne hanno effettivamente Unicode. Pertanto ho contrassegnato la risposta di Joe come risposta accettata. Ho votato a favore delle altre risposte che mi hanno anche aiutato.
John G Hohengarten,

@JohnGHohengarten e Joe: va bene. Non ho menzionato la domanda poiché era in questa risposta e in quella di Scott. Direi solo che non è necessario convertire la NVARCHARcolonna in NVARCHARquanto è già quel tipo. E non sono sicuro di come hai determinato il carattere non convertibile, ma puoi convertire la colonna in VARBINARYper ottenere le sequenze di byte UTF-16. E UTF-16 è in ordine di byte inverso, quindi p= 0x7000e quindi si invertono quei due byte per ottenere il punto di codice U+0070. Ma, se l'origine è VARCHAR, non può essere un carattere Unicode. Qualcos'altro sta succedendo. Hai bisogno di maggiori informazioni.
Solomon Rutzky,

@srutzky Ho aggiunto il cast per evitare problemi di precedenza sul tipo di dati. Potresti aver ragione sul fatto che non è necessario. Per l'altra domanda, ho suggerito UNICODE () e SUBSTRING (). L'approccio funziona?
Joe Obbish,

@JohnGHohengarten e Joe: la precedenza del tipo di dati non dovrebbe essere un problema in quanto VARCHARverrà convertita implicitamente in NVARCHAR, ma potrebbe essere meglio farlo CONVERT(NVARCHAR(80), CONVERT(VARCHAR(80), column)) <> column. SUBSTRINGa volte funziona, ma non funziona con i Personaggi Supplementari quando si usano le Collation che non finiscono _SC, e quello che John sta usando non lo fa, anche se probabilmente non è un problema qui. Ma la conversione in VARBINARY funziona sempre. E CONVERT(VARCHAR(10), CONVERT(NVARCHAR(10), '›'))non risulta ?, quindi vorrei vedere i byte. Il processo ETL potrebbe averlo convertito.
Solomon Rutzky,

5

Prima di fare qualsiasi cosa, si prega di considerare le domande poste da @RDFozz in un commento sulla domanda, vale a dire:

  1. Ci sono tutte le altre fonti oltre [Q].[G]che popolano questo tavolo?

    Se la risposta è diversa da "Sono sicuro al 100% che questa è l' unica fonte di dati per questa tabella di destinazione", non apportare alcuna modifica, indipendentemente dal fatto che i dati attualmente nella tabella possano essere convertiti o meno senza perdita di dati.

  2. Ci sono eventuali piani / discussioni relative all'aggiunta di ulteriori fonti per compilare questi dati in un prossimo futuro?

    E vorrei aggiungere una questione connessa: v'è stata alcuna discussione intorno supporto di più lingue nella tabella di origine corrente (cioè [Q].[G]) convertendo è di NVARCHAR?

    Dovrai chiedere in giro per avere un'idea di queste possibilità. Suppongo che al momento non ti sia stato detto nulla che indichi in questa direzione altrimenti non porteresti questa domanda, ma se queste domande sono state ritenute "no", allora devono essere poste e poste pubblico sufficientemente ampio per ottenere la risposta più accurata / completa.

Il problema principale qui non è tanto avere punti di codice Unicode che non possono essere convertiti (mai), ma piuttosto avere punti di codice che non rientrano tutti in una singola pagina di codice. Questa è la cosa bella di Unicode: può contenere caratteri di TUTTE le pagine di codice. Se esegui la conversione da NVARCHAR- dove non devi preoccuparti delle pagine di codice - in VARCHAR, dovrai assicurarti che le regole di confronto della colonna di destinazione utilizzino la stessa pagina di codice della colonna di origine. Ciò presuppone di avere una sola fonte o più fonti che utilizzano la stessa tabella codici (non necessariamente la stessa fascicolazione). Ma se ci sono più origini con più pagine di codice, è possibile che si verifichi il seguente 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;

Resi (2 ° set di risultati):

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

Come puoi vedere, tutti quei personaggi possono essere convertiti VARCHAR, ma non nella stessa VARCHARcolonna.

Utilizzare la seguente query per determinare qual è la tabella codici per ogni colonna della tabella di origine:

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

DETTO CIÒ....

Hai detto di essere su SQL Server 2008 R2, MA, non hai detto quale edizione. SE ti trovi in ​​Enterprise Edition, quindi dimentica tutte queste cose di conversione (poiché probabilmente lo stai facendo solo per risparmiare spazio) e abilita la compressione dei dati:

Implementazione della compressione Unicode

Se si utilizza Standard Edition (e ora sembra che tu sia 😞) allora c'è un'altra possibilità di tiro a segno: l'aggiornamento a SQL Server 2016 poiché SP1 include la possibilità per tutte le versioni di utilizzare la compressione dei dati (ricorda, ho detto "long-shot "😉).

Naturalmente, ora che è stato appena chiarito che esiste solo una fonte per i dati, allora non hai nulla di cui preoccuparti poiché la fonte non può contenere caratteri solo Unicode o caratteri al di fuori del suo codice specifico pagina. In tal caso, l'unica cosa di cui dovresti essere consapevole è usare la stessa Fascicolazione della colonna di origine, o almeno una che sta usando la stessa pagina di codice. Significato, se si utilizza la colonna di origine SQL_Latin1_General_CP1_CI_AS, è possibile utilizzare Latin1_General_100_CI_ASa destinazione.

Una volta che sai quale Collation usare, puoi:

  • ALTER TABLE ... ALTER COLUMN ...essere VARCHAR(assicurarsi di specificare la corrente NULL/ NOT NULLimpostazione), che richiede un po 'di tempo e molto spazio nel registro delle transazioni per 87 milioni di righe, OPPURE

  • Crea nuove colonne "ColumnName_tmp" per ognuna e popolare lentamente UPDATEfacendo TOP (1000) ... WHERE new_column IS NULL. Una volta popolate tutte le righe (e convalidate che siano state copiate tutte correttamente! Potrebbe essere necessario un trigger per gestire gli AGGIORNAMENTI, se presenti), in una transazione esplicita, utilizzare sp_renameper scambiare i nomi delle colonne delle "attuali" colonne da " _Vecchio "e quindi le nuove colonne" _tmp "per rimuovere semplicemente" _tmp "dai nomi. Quindi chiama sp_reconfigurela tabella per invalidare tutti i piani memorizzati nella cache che fanno riferimento alla tabella e se ci sono viste che fanno riferimento alla tabella, dovrai chiamare sp_refreshview(o qualcosa del genere). Dopo aver convalidato l'app e ETL funziona correttamente con essa, è possibile eliminare le colonne.


Ho eseguito la query CodePage che hai fornito sia sull'origine che sulla destinazione e CodePage è 1252 e collation_name è SQL_Latin1_General_CP1_CI_AS su ENTRAMBI l'origine E la destinazione.
John G Hohengarten,

@JohnGHohengarten Ho appena aggiornato di nuovo, in fondo. Per essere facile puoi mantenere la stessa raccolta, anche se Latin1_General_100_CI_ASè molto meglio di quella che stai usando. Semplice significato che il comportamento di ordinamento e confronto sarà lo stesso tra di loro, anche se non buono come il nuovo Collation che ho appena citato.
Solomon Rutzky,

4

Ho una certa esperienza con questo da quando avevo un vero lavoro. Poiché all'epoca volevo preservare i dati di base e dovevo anche tenere conto di nuovi dati che potevano avere caratteri che si sarebbero persi nel riordino, sono andato con una colonna calcolata non persistente.

Ecco un breve esempio usando una copia del database Super User dal dump dei dati SO .

Possiamo vedere subito che ci sono DisplayNames con caratteri Unicode:

Noccioline

Quindi aggiungiamo una colonna calcolata per capire quanti! La colonna 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

Il conteggio restituisce ~ 3000 righe

Noccioline

Il piano di esecuzione è un po 'trascinante, però. La query termina rapidamente, ma questo set di dati non è eccessivamente grande.

Noccioline

Poiché per aggiungere un indice non è necessario mantenere persistenti le colonne calcolate, possiamo eseguire una di queste operazioni:

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

Il che ci dà un piano leggermente più ordinato:

Noccioline

Capisco se questa non è la risposta, dal momento che comporta modifiche architettoniche, ma considerando la dimensione dei dati, probabilmente stai cercando di aggiungere indici per far fronte a domande che si uniscono alla tabella comunque.

Spero che sia di aiuto!


1

Utilizzando l'esempio in Come verificare se un campo contiene dati unicode , è possibile leggere i dati in ciascuna colonna e procedere come CASTsegue:

--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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.