检测NVARCHAR列中的任何值是否实际上是unicode


14

我继承了一些SQL Server数据库。SQL Server 2014 Standard的源数据库(我称为“ Q”)中有一张表(我称为“ G”),约有8670万行,宽41列,可将ETL转换为在SQL Server 2008 R2 Standard上具有相同表名的目标数据库(我称其为“ P”)。

即[Q]。[G] ---> [P]。[G]

编辑:3/20/2017:有人问源表是否是目标表的唯一源。是的,这是唯一的来源。就ETL而言,没有任何真正的变化发生。它实际上是源数据的1:1副本。因此,没有计划向此目标表添加其他源。

[Q]。[G]中的一半以上的列是VARCHAR(源表):

  • 列中的13个是VARCHAR(80)
  • 9列是VARCHAR(30)
  • 列中的2个是VARCHAR(8)。

同样,[P]。[G]中的相同列是NVARCHAR(目标表),具有相同宽度的相同列数。(换句话说,长度相同,但为NVARCHAR)。

  • 列中的13个是NVARCHAR(80)
  • 9列是NVARCHAR(30)
  • 列中的2个是NVARCHAR(8)。

这不是我的设计。

我想更改[P]。[G](目标)列数据类型,从NVARCHAR到VARCHAR。我想安全地做到这一点(不会因转换而丢失数据)。

如何查看目标表中每个NVARCHAR列中的数据值,以确认该列是否实际包含任何Unicode数据?

可以检查每个NVARCHAR列的每个值(循环吗?)并告诉我其中的任何值是否为Unicode的查询(DMVs?)是理想的解决方案,但欢迎使用其他方法。


2
首先,考虑您的过程以及如何使用数据。中的数据[G]被ETL传送到[P]。如果[G]varchar,则ETL进程是数据进入的唯一方式[P],那么除非该进程添加了真正的Unicode字符,否则不应该有任何字符。如果其他进程在中添加或修改了数据[P],则您需要格外小心-仅仅因为当前所有数据都可以varchar,并不意味着nvarchar明天就不能添加数据。同样,有可能正在消耗[P]需求nvarchar数据中的任何数据。
RDFozz

Answers:


10

假设您的一列不包含任何unicode数据。为了验证您需要读取每一行的列值。除非您在具有行存储表的列上有索引,否则将需要从表中读取每个数据页。考虑到这一点,我认为将所有列检查组合到针对该表的单个查询中非常有意义。这样,您将不会多次读取表的数据,也不必编写游标或其他某种形式的循环。

要检查单个列,请相信您可以执行以下操作:

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

NVARCHAR到强制转换VARCHAR应为您提供相同的结果,除非存在Unicode字符。Unicode字符将转换为?。因此,以上代码应NULL正确处理案例。您有24列要检查,因此您可以使用标量聚合检查单个查询中的每一列。下面是一种实现:

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

对于每一列,1如果其任何值包含unicode,都会得到结果。结果0意味着可以安全地转换所有数据。

我强烈建议您使用新的列定义制作表的副本,然后在其中复制数据。如果就地进行转换,您将进行昂贵的转换,因此制作副本可能不会那么慢。拥有副本意味着您可以轻松地验证所有数据仍然存在(一种方法是使用EXCEPT关键字),并且可以非常轻松地撤消该操作。

另外,请注意,您当前可能没有任何Unicode数据,将来的ETL可能会将Unicode加载到以前干净的列中。如果在ETL流程中未对此进行检查,则应考虑在进行此转换之前添加该内容。


尽管@srutzky的答案和讨论都很好并且提供了有用的信息,但Joe为我提供了我的问题要问的问题:一个查询,告诉我列中是否有任何值实际上具有Unicode。因此,我将Joe的答案标记为已接受的答案。我投票给其他对我也有帮助的答案。
John G Hohengarten

@JohnGHohengarten和Joe:很好。我没有提到查询,因为它和Scott都在此答案中。我只想说,没有必要将NVARCHAR列转换NVARCHAR为该类型。并不确定如何确定不可转换的字符,但是可以将列转换为VARBINARY以获取UTF-16字节序列。UTF-16是反向字节顺序,因此p= 0x7000,然后反转这两个字节即可得到Code Point U+0070。但是,如果源是VARCHAR,则它不能是Unicode字符。发生了其他事情。需要更多信息。
所罗门·鲁兹基

@srutzky我添加了强制类型转换以避免数据类型优先级问题。您可能是对的,不需要它。对于另一个问题,我建议使用UNICODE()和SUBSTRING()。这种方法行得通吗?
Joe Obbish

@JohnGHohengarten和Joe:数据类型优先级不应该成为VARCHAR隐式转换为的问题NVARCHAR,但是这样做可能更好CONVERT(NVARCHAR(80), CONVERT(VARCHAR(80), column)) <> columnSUBSTRING有时可以,但是当使用不以结尾的归类时,它不能与辅助字符一起使用_SC,而John使用的不是,但在这里不太可能出现问题。但是始终可以转换为VARBINARY。并且CONVERT(VARCHAR(10), CONVERT(NVARCHAR(10), '›'))不会导致?,因此我想查看字节。ETL流程可能已将其转换。
所罗门·鲁兹基

5

在执行任何操作之前,请在对问题的评论中考虑@RDFozz提出的问题,即:

  1. 是否有任何除了其他来源[Q].[G]填充这个表?

    如果响应超出“我100%确信这是此目标表的唯一数据源”之外的任何内容,则不要进行任何更改,无论是否可以转换表中当前的数据而不必数据丢失。

  2. 是否有任何计划/讨论与增加其他来源以在不久的将来填充此数据有关?

    我还要添加一个相关的问题:在当前的源表(即[Q].[G])中通过将转换来支持多种语言是否有讨论NVARCHAR

    您将需要四处询问以了解这些可能性。我假设您目前尚未被告知任何指向该方向的信息,否则您将不会问这个问题,但是如果假设这些问题为“否”,则需要提出这些问题,并要求他们足够多的受众获得最准确/完整的答案。

这里的主要问题不是具有无法转换的Unicode代码点,而是具有无法全部转换为单个代码页的代码点。关于Unicode的好处是:它可以保存所有代码页中的字符。如果您从NVARCHAR不需要担心代码页的地方转换为VARCHAR,则需要确保目标列的归类使用与源列相同的代码页。假定使用同一代码页(虽然不一定是同一归类)有一个源或多个源。但是,如果存在具有多个代码页的多个源,那么您可能会遇到以下问题:

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;

返回(第二个结果集):

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

如您所见,所有这些字符都可以转换为VARCHAR,只是不能在同一个字符中VARCHAR列中。

使用以下查询来确定源表的每一列的代码页是什么:

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

话虽如此....

您提到在SQL Server 2008 R2上,但没有说什么版本。如果您碰巧使用的是Enterprise Edition,则请忽略所有这些转换内容(因为您可能只是为了节省空间而这样做),然后启用数据压缩:

Unicode压缩实现

如果使用标准版(现在看来您是),则还有另一种可能:升级到SQL Server 2016,因为SP1包括所有版本都可以使用数据压缩的功能(请记住,我确实说过“ “😉)。

当然,既然已经澄清了只有一个数据源,那么您不必担心,因为该源不能包含任何仅Unicode字符或特定代码之外的字符。页。在这种情况下,您唯一需要注意的是使用与源列相同的归类,或者至少使用相同的代码页。意思是,如果源列使用SQL_Latin1_General_CP1_CI_AS,则可以使用Latin1_General_100_CI_AS在目标位置。

知道要使用的排序规则后,您可以:

  • ALTER TABLE ... ALTER COLUMN ...VARCHAR(一定要指定当前NULL/ NOT NULL设置),这需要一点时间和大量的事务日志空间来存储8700万行,或者

  • 为每个新的一个“ColumnName_tmp”栏目,并通过慢慢填充UPDATETOP (1000) ... WHERE new_column IS NULL。填充所有行(并确认所有行均已正确复制后!您可能需要触发器来处理UPDATE,如果有的话),在显式事务中,用于sp_rename将“当前”列的列名交换为“ _Old”,然后新建“ _tmp”列,以从名称中删除“ _tmp”。然后,调用sp_reconfigure该表以使引用该表的所有缓存计划都无效,并且如果有任何引用该表的View,则需要调用sp_refreshview(或类似方法)。验证应用程序并且ETL可以正常使用后,您可以删除列。


我在源和目标上都运行了您提供的CodePage查询,CodePage是1252,collat​​ion_name是在源AND目标上都为SQL_Latin1_General_CP1_CI_AS。
John G Hohengarten

@JohnGHohengarten我再次在底部更新。为简单起见,您可以保留相同的归类,即使Latin1_General_100_CI_AS比使用的归类要好得多。简单的意思是,即使它们之间的排序和比较行为也一样,即使不如我刚才提到的新排序规则那样好。
所罗门·鲁兹基

4

当我有一份真正的工作时,从背面我对此有一些经验。由于当时我想保留基础数据,并且还必须考虑可能包含可能在改组中丢失的字符的新数据,因此我使用了非持久性计算列。

这是一个使用SO数据转储中的超级用户数据库副本的快速示例

我们马上就可以看到存在带Unicode字符的DisplayName:

坚果类

因此,让我们添加一个计算列以找出多少!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

计数返回〜3000行

坚果类

不过,执行计划有点拖累。查询快速完成,但是此数据集并不是很大。

坚果类

由于不需要持久保存计算列来添加索引,因此我们可以执行以下操作之一:

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

这给了我们一个稍微整洁的计划:

坚果类

我明白,如果这不是的答案,因为它涉及的架构改变,但考虑到数据的大小,你可能看添加索引,以应付查询,自我反正加入表。

希望这可以帮助!


1

使用如何检查字段中是否包含unicode数据中的示例,您可以读取每一列中的数据,并在CAST下面进行和检查:

--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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.