将utf-8编码的varbinary(max)数据转换为nvarchar(max)字符串

时间:2019-02-11 08:21:43

标签: sql-server tsql

在T-SQL中是否有一种简单的方法将utf-8编码的varbinary(max)列转换为varchar(max)。类似于CONVERT(varchar(max), [MyDataColumn])。最好的解决方案是不需要自定义功能。 目前,我在客户端转换数据,但这有一个缺点,即正确的过滤和排序效率不如服务器端。

3 个答案:

答案 0 :(得分:4)

SQL-Server不知道UTF-8(至少可以有效使用的所有版本)。有limited support starting with v2014 SP2(以及有关supported versions的一些详细信息)  通过utf-8从光盘读取BCP编码文件时(与将内容写入光盘相同)。

重要信息:

VARCHAR(x)不是utf-8 。它是 1字节编码扩展ASCII码,使用代码页(存在于归类中)作为字符映射。

NVARCHAR(x)不是 utf-16(但非常接近,它是ucs-2。这是一个 2字节编码的字符串,几乎覆盖了所有已知字符(但存在例外)。

utf-8将使用1个字节表示纯拉丁语字符,但是将2个或更多字节用于已编码的外来字符集。

VARBINARY(x)utf-8保留为无意义的字节链。

简单的CASTCONVERT将不起作用:VARCHAR将每个单个字节当作一个字符。当然,这不是您期望的结果。 NVARCHAR将2字节的每个块当作一个字符。再次不是您需要的东西。

您可以尝试将其写出到文件中,然后使用BCP(v2014 SP2或更高版本)将其读回。但是,我为您带来的更好机会是CLR function

答案 1 :(得分:0)

您可以使用以下内容将字符串发布到varbinary字段中

Encoding.Unicode.GetBytes(Item.VALUE)

然后使用以下内容以字符串形式检索数据

public string ReadCString(byte[] cString)
        {
            var nullIndex = Array.IndexOf(cString, (byte)0);
            nullIndex = (nullIndex == -1) ? cString.Length : nullIndex;
            return System.Text.Encoding.Unicode.GetString(cString);
        }

答案 2 :(得分:0)

更改表格技巧

我试图使用SQL Server 2019的Utf8归类方法,到目前为止我发现了一种可能的方法,该方法应该比XML技巧要快(见下文)。

  1. 使用varbinary列创建临时表。
  2. 将varbinary值插入表
  3. 通过utf8归类将表更改为varchar的列

(此括号用于将编号列表与格式化代码分开)

drop table if exists
  #bin,
  #utf8;

create table #utf8 (UTF8 VARCHAR(MAX) COLLATE Czech_100_CI_AI_SC_UTF8);
create table #bin (BIN VARBINARY(MAX));

insert into #utf8 (UTF8) values ('Žluťoučký kůň říčně pěl ďábelské ódy za svitu měsíce.');
insert into #bin (BIN) select CAST(UTF8 AS varbinary(max)) from #utf8;

select * from #utf8; --here you can see the utf8 string is stored correctly and that
select BIN, CAST(BIN AS VARCHAR(MAX)) from #bin; --utf8 binary is converted into gibberish

alter table #bin alter column BIN varchar(max) collate Czech_100_CI_AI_SC_UTF8;
select * from #bin; --voialá, correctly converted varchar

alter table #bin alter column BIN nvarchar(max);
select * from #bin; --finally, correctly converted nvarchar 

XML技巧

以下解决方案应适用于任何编码。

有一种棘手的方法可以准确地执行OP的要求。编辑:我发现了在SO(SQL - UTF-8 to varchar/nvarchar Encoding issue)上讨论过的相同方法

过程如下:

SELECT
  CAST(
    '<?xml version=''1.0'' encoding=''utf-8''?>' +
    REPLACE(
      REPLACE(
        @BinaryValue,
        '&',
        '&amp;'
      ),
      '<',
      '&lt;'
    ) AS XML
  ).value('.', 'varchar(max)')

为什么起作用:varbinary和varchar是相同的位字符串-只有解释不同,所以生成的xml确实是utf8编码的位流,并且xml解释器才能够重建正确的utf8编码的字符。

速度差

速度差异取决于数据。当使用XML trick时有很多要替换的地方时,ALTER trick可以快得多。当文本很少或文本较短时,创建和更改临时表的开销将导致较大的开销。所以:“取决于...”

DECLARE @LongText NVARCHAR(MAX) = N'...<s>o</s>o&amp;me lo>>>ng u>n<<i< &&code text...';
DECLARE @StartXML DATETIME2(7), @EndXML DATETIME2(7), @StartTable DATETIME2(7), @EndTable DATETIME2(7);

drop table if exists
  #longTexts,
  #longBinaries,
  #XMLConverts;
create table #longTexts (LongText VARCHAR(MAX) COLLATE Czech_100_CI_AI_SC_UTF8 NOT NULL);
create table #longBinaries (LongBinary VARBINARY(MAX) NOT NULL);
CREATE TABLE #XMLConverts (LongText VARCHAR(MAX) COLLATE Czech_100_CI_AI_SC_UTF8 NOT NULL);

insert into #longTexts --make the long text longer
  (LongText)
select
  REPLICATE(@LongText, 100000)
from 
  Data0001.dbo.Numbers --use while if you don't have number table
WHERE
  Number BETWEEN 1 AND 100; --make more of them

insert into #longBinaries (LongBinary) select CAST(LongText AS varbinary(max)) from #longTexts;

SET @StartXML = SYSDATETIME();
------------------------------
--MEASURE XML--
INSERT INTO #XMLConverts 
  (
    LongText
  )
SELECT
  CAST(
    '<?xml version=''1.0'' encoding=''utf-8''?>' +
    REPLACE(
      REPLACE(
        LB.LongBinary,
        '&',
        '&amp;'
      ),
      '<',
      '&lt;'
    ) AS XML
  ).value('.', 'varchar(max)')
FROM
  #longBinaries AS LB;
SET @EndXML = SYSDATETIME();

SET @StartTable = SYSDATETIME();
--------------------------------------------
--MEASURE ALTER TABLE--
DROP TABLE IF EXISTS #AlterConverts;
CREATE TABLE #AlterConverts (UTF8 VARBINARY(MAX));

INSERT INTO #AlterConverts 
  (
    UTF8
  )
SELECT
  LB.LongBinary
FROM
  #longBinaries AS LB;

ALTER TABLE #AlterConverts ALTER COLUMN UTF8 VARCHAR(MAX) COLLATE Czech_100_CI_AI_SC_UTF8;
--ALTER TABLE #AlterConverts ALTER COLUMN UTF8 NVARCHAR(MAX);

SET @EndTable = SYSDATETIME();

SELECT
  DATEDIFF(MILLISECOND, @StartXML, @EndXML) AS XML_MS,
  DATEDIFF(MILLISECOND, @StartTable, @EndTable) AS ALTER_MS;