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
确实会产生一个唯一的结果集。
我也尝试在VARCHAR
和NVARCHAR
和到不同的排序规则之间进行转换(值在同一列中,在使用 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 BY
、ORDER BY
、任何窗口函数和COUNT
。
到目前为止调查工作做得很好。一些初步的笔记:
我不会担心 SO 答案中的该功能。
RTRIM
并且LTRIM
只修剪空格,而不是一般的空白:
SELECT RTRIM('A ') + 'a';
-- Aa
SELECT RTRIM('A ' + CHAR(9)) + 'a'; -- CHAR(9) = tab
-- A a
Run Code Online (Sandbox Code Playgroud)添加GROUP BY
(第二个查询)不会更改该查询,因为它隐含在第一个查询中 ;-)。
无论0xA000
是 2 个VARCHAR
字符还是 1 个NVARCHAR
字符,对于任一数据类型,使用Latin1_General_CI_AI
或 的此字节序列似乎没有任何特殊行为Latin1_General_100_CI_AI
。
尽管如此,还是有些不对劲。您根本不能使用相同的二进制值具有不同的长度(或不同的任何东西)。长度是如何确定的:LEN
或DATALENGTH
?该值是否有可能在测试中的某处被截断,以便看起来相同?
为了进一步提供帮助,我们需要知道两件事(请用结果更新问题):
的确切数据类型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)以下查询的输出:
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)。
很清楚:
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)
需要明确的是,所有这些与VARBINARY
andVARCHAR
等相关的工作都是不必要的。
但这不会长期有效,因为用户可以更新此表,我需要调整每个查询以在
WHERE
子句中进行替换。
诚然,更新每个 WHERE 子句并不是一个可行的解决方案。这就是为什么您需要在输入的过程中清理输入。数据的入口点数量有限(UI 的 INSERT / UPDATE 过程,可能是一些 ETL 过程),所以它应该不会那么糟糕。您可以要求开发人员在调用存储过程之前去掉“坏”字符,但不能保证他们会,或者新代码会,或者以后不会改变,或者他们会能够修复 ETL 过程等。