为什么两个相同的字符串长度不同但二进制值相同?

Wor*_*DBA 7 sql-server collation distinct sql-server-2016

我试图从表中返回一组不同的部门名称 - 没什么特别的。但是,使用以下查询时会显示重复项:

  select distinct department_name
    from dbo.departments;
Run Code Online (Sandbox Code Playgroud)

我也试过:

  select distinct department_name
    from dbo.departments
group by department_name;
Run Code Online (Sandbox Code Playgroud)

所以这让我相信我可能在值中隐藏了字符,果然,当我检查字符串的长度时,它们返回了不同的值。所以,我决定使用堆栈溢出这个问题中的函数来定位隐藏字符。奇怪的是,这只返回 SPACE。然后我尝试了以下查询,它根本没有区别:

select distinct ltrim(rtrim(department_name)) as department_name
  from dbo.departments;
Run Code Online (Sandbox Code Playgroud)

出于好奇,我将这些值转换为VARBINARY并注意到它们具有完全相同的二进制值,并且对二进制值执行 aDISTINCT确实会产生一个唯一的结果集。

我也尝试在VARCHARNVARCHAR和到不同的排序规则之间进行转换(值在同一列中,在使用 Latin1_General_CI_AI 的同一数据库中)。我真的需要能够从这张表中得到一个不同的集合。有谁知道可能导致这个问题的原因是什么?

更新

经过进一步调查,这个问题似乎只发生在以十六进制值结尾的字符串中0xA000。列中不以该字符结尾的任何值都可以。

更新 2

如果我0xA000从字符串中删除字符,我可以DISTINCT像这样正常应用:

DECLARE @binary VARBINARY(8) = 0xA000;
DECLARE @string VARCHAR(8) = CONVERT(VARCHAR(MAX), @binary);
 UPDATE dbo.departments
    SET department_name = REPLACE(department_name, @string, '');
Run Code Online (Sandbox Code Playgroud)

但这不会长期有效,因为用户可以更新此表,我需要调整每个查询以在WHERE子句中进行替换。我现在正在使用一种解决方法,它只不过是MIN用于返回长度最短的条目。这不太理想,因为 distinct 的问题也会影响大多数其他语言元素,例如GROUP BYORDER BY、任何窗口函数和COUNT

Sol*_*zky 6

到目前为止调查工作做得很好。一些初步的笔记:

  1. 我不会担心 SO 答案中的该功能。

  2. RTRIM并且LTRIM只修剪空格,而不是一般的空白:

    SELECT RTRIM('A    ') + 'a';
    -- Aa
    
    SELECT RTRIM('A    ' + CHAR(9)) + 'a'; -- CHAR(9) = tab
    -- A        a
    
    Run Code Online (Sandbox Code Playgroud)
  3. 添加GROUP BY(第二个查询)不会更改该查询,因为它隐含在第一个查询中 ;-)。

  4. 无论0xA000是 2 个VARCHAR字符还是 1 个NVARCHAR字符,对于任一数据类型,使用Latin1_General_CI_AI或 的此字节序列似乎没有任何特殊行为Latin1_General_100_CI_AI

尽管如此,还是有些不对劲。您根本不能使用相同的二进制值具有不同的长度(或不同的任何东西)。长度是如何确定的:LENDATALENGTH?该值是否有可能在测试中的某处被截断,以便看起来相同?

为了进一步提供帮助,我们需要知道两件事(请用结果更新问题):

  1. 的确切数据类型department_name。请通过以下方式查找:

    SELECT typ.[name], col.*
    FROM   sys.columns col
    INNER JOIN sys.types typ
            ON typ.[user_type_id] = col.[user_type_id]
    WHERE  col.[object_id] = OBJECT_ID(N'dbo.departments')
    AND    col.[name] = N'department_name';
    
    Run Code Online (Sandbox Code Playgroud)
  2. 以下查询的输出:

    SELECT dept.department_name,
           LEN(dept.department_name) AS [name_chars],
           DATALENGTH(dept.department_name) AS [name_bytes],
           CONVERT(VARBINARY(MAX), dept.department_name) AS [name_hex]
    FROM   dbo.departments dept
    ORDER BY dept.department_name
    
    Run Code Online (Sandbox Code Playgroud)

    只需找到一组看起来相同但通过DISTINCT.

目前,我可以说,如果您的值中有字符 0(ASCII 值 0,代码点 U+0000, CHAR(0), NCHAR(0)),那么外观可能具有欺骗性,因为 char(0) 是字符串的“空终止符”。所以它和它之后的任何东西都不会被显示,但它和它之后的一切仍然是字符串的一部分:

DECLARE @Test TABLE
(
  [Something] VARCHAR(50) COLLATE Latin1_General_CI_AI
);

INSERT INTO @Test ([Something])
VALUES ('Sometimes you' + CHAR(0) + 'feel like a nut');

INSERT INTO @Test ([Something])
VALUES ('Sometimes you' + CHAR(0) + 'feel like a nut');

INSERT INTO @Test ([Something])
VALUES ('Sometimes you' + CHAR(0) + 'don''t');

SELECT DISTINCT [Something],
       LEN([Something]) AS [Something_chars],
       DATALENGTH([Something]) AS [Something_bytes]
FROM   @Test;
/*
Something        Something_chars    Something_bytes
Sometimes you    19                 19
Sometimes you    29                 29
*/
Run Code Online (Sandbox Code Playgroud)

欣赏:彼得·保罗·阿尔蒙德·乔伊和土墩 - “感觉像个疯子”(1980)


临时更新(等待通过查询输出更新问题):

根据评论中的信息:

我同时使用 LEN 和 DATALENGTH 来执行检查。对于两个完全相同的字符串,我分别得到 (20,40) 和 (21,42)。

很清楚:

  1. 这些绝对是不同的值,并且
  2. 列的数据类型是NVARCHAR(因为DATALENGTH是两次LEN

这意味着该0xA000值是单个 UTF16LE 字符。由于是小端(字节顺序相反),实际的代码点是 U+00A0。那个性格是:

不间断空间

就像我们最喜欢的 HTML 字符一样: 

您需要做的就是在进入数据库的途中删除这些字符,使用:

REPLACE(@InputParam, NCHAR(0x00A0) COLLATE Latin1_General_100_BIN2, N'')
Run Code Online (Sandbox Code Playgroud)

例如:

SELECT CONVERT(VARBINARY(MAX),
      REPLACE(N'test' + NCHAR(0x00A0), NCHAR(0x00A0) COLLATE Latin1_General_100_BIN2, N'')
    );
Run Code Online (Sandbox Code Playgroud)

需要明确的是,所有这些与VARBINARYandVARCHAR等相关的工作都是不必要的。

但这不会长期有效,因为用户可以更新此表,我需要调整每个查询以在WHERE子句中进行替换。

诚然,更新每个 WHERE 子句并不是一个可行的解决方案。这就是为什么您需要在输入的过程中清理输入。数据的入口点数量有限(UI 的 INSERT / UPDATE 过程,可能是一些 ETL 过程),所以它应该不会那么糟糕。您可以要求开发人员在调用存储过程之前去掉“坏”字符,但不能保证他们会,或者新代码会,或者以后不会改变,或者他们会能够修复 ETL 过程等。