如何查找具有多个连续大写字符的值

Meg*_*s D 6 sql-server collation t-sql sql-server-2014 unicode

对于我正在处理的项目,我需要识别大写不正确的值。

例如,我需要它来识别以下类型的值:

Mr JOHN Smith
MR John Smith
Mr John SMITH
Mr JOhn Smith
Run Code Online (Sandbox Code Playgroud)

到目前为止我尝试过的想法:

Select * from table where Name = upper(Name) 
  collate SQL_Latin1_General_CP1_CS_AS
Run Code Online (Sandbox Code Playgroud)

(它给出了全部大写的行的结果,例如 MR JOHN SMITH)

Select * from table where right(Name,3) = upper(right(Name,3)) 
  collate SQL_Latin1_General_CP1_CS_AS
Run Code Online (Sandbox Code Playgroud)

它也拾取了一些行。

但我怀疑这是解决此问题的最有效方法。

Sol*_*zky 7

这个问题比表面上看起来要复杂得多(因此答案比大多数人预期的要长)。如果要搜索的字符串是代码(邮政编码、ISO 国家/地区代码、ISO 州代码、SKU 等),或者使用的字符是所有语言的所有可能字母的有限子集,那么这将是相当简单的。但是在处理人名的时候,就没有这样的运气了。

                        All of the following example code and test cases can be found on Pastebin ( Searching
for Case-Sensitive patterns in SQL Server
). The SQL posted on Pastebin includes
additional test cases and additional examples related to various points mentioned below.

非常简单,您可以执行以下操作,这适用于示例数据。我添加了两个测试用例来帮助确定这个(或任何)方法是否有效,因为它是应该匹配的数据。

SET NOCOUNT ON;
DECLARE @SampleData TABLE
(
  Name NVARCHAR(100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL
);
INSERT INTO @SampleData (Name) VALUES (N'Mr JOHN Smith');
INSERT INTO @SampleData (Name) VALUES (N'MR John Smith');
INSERT INTO @SampleData (Name) VALUES (N'Mr John SMITH');
INSERT INTO @SampleData (Name) VALUES (N'Mr JOhn Smith');
INSERT INTO @SampleData (Name) VALUES (N'mr john smith'); -- all lower-case
INSERT INTO @SampleData (Name) VALUES (N'Mr John Smith'); -- proper-case

SELECT *
FROM @SampleData
WHERE Name COLLATE Latin1_General_100_CS_AS
                 LIKE N'%[ABCDEFGHIJKLMNOPQRSTUVWXYZ][ABCDEFGHIJKLMNOPQRSTUVWXYZ]%';
Run Code Online (Sandbox Code Playgroud)

请注意:

  1. 我没有使用以 开头的排序规则,SQL_因为自 SQL Server 2000 发布以来,这些排序规则已被弃用(或者更准确地说:已过时)。

  2. 我正在使用100系列中的排序规则,因为它是最新的,并且应该从 SQL Server 2008 开始可用。并非所有排序规则都有100系列,但最好使用最新的排序规则,可能是90.

  3. 我没有使用二进制排序规则(即,自 SQL Server 2005 发布以来,排序规则已被弃用,_BIN2或者即使排序规则已被弃用,因此仅在需要二进制排序规则时使用)。二进制排序规则不“区分大小写”;它们是“字节敏感” ( ) / “代码点敏感” ( ) 并且这些是非常不同的概念。在使用(或测试)仅限美国英语的字符时,二进制排序规则似乎只区分大小写。使用二进制排序规则时,任何带有重音符号的字符都可能被错误地处理。例如:_BIN_BIN_BIN2_BIN_BIN2

    PRINT N'U' + NCHAR(0x0308) +
       N' <-- U + NCHAR(0x0308) -- combining diaeresis [1 "character" from 2 code points / 4 bytes]';
       -- U?
    PRINT NCHAR(220) + N' <-- NCHAR(220) [1 "character" from 1 code point / 2 bytes]'; -- Ü
    PRINT NCHAR(252) + N' <-- NCHAR(252) [1 "character" from 1 code point / 2 bytes]'; -- ü
    PRINT '';
    
    IF (NCHAR(252) = N'U'+NCHAR(0x0308) COLLATE Latin1_General_100_CI_AS)
            PRINT 'Same!' ELSE PRINT 'Nope.'; -- case INsensitive
    IF (NCHAR(252) = N'U'+NCHAR(0x0308) COLLATE Latin1_General_100_CS_AS)
            PRINT 'Same!' ELSE PRINT 'Nope.'; -- case SENSITIVE
    IF (NCHAR(220) = N'U'+NCHAR(0x0308) COLLATE Latin1_General_100_CS_AS)
            PRINT 'Same!' ELSE PRINT 'Nope.'; -- case SENSITIVE
    IF (NCHAR(220) = N'U'+NCHAR(0x0308) COLLATE Latin1_General_100_BIN2)
            PRINT 'Same!' ELSE PRINT 'Nope.'; -- Binary
    
    Run Code Online (Sandbox Code Playgroud)

    要更深入地了解该主题,请参阅我的博客文章:不,二进制排序规则不区分大小写

  4. 我没有使用单个字符范围(即[A-Z]LIKE和中起作用的PATINDEX),而是添加了 26 个字母中的每一个。这是必需的,以便首选_CS(即区分大小写)排序规则正常工作。如果您通过[A-Z]语法指定一个字符范围,那么它似乎不遵守区分大小写的排序规则。然而,这是对行为的错误解释。区分大小写的比较可能很棘手,因为根据设计,它们在相等比较之间的工作方式不同(例如WHERE N'zbz' LIKE N'%B%'WHERE N'b' = N'B')和范围比较(例如WHERE N'zbz' LIKE N'%[A-C]%')。范围比较更像是排序。而且,区分大小写的排序不会在另一种情况之前处理所有一种情况。考虑字典的排序方式(实际上,Microsoft 文档甚至使用术语“字典排序”来描述非二进制排序规则):它们不会将所有以 AZ 开头的单词放在以 az 开头的单词之前。他们将每个字母的大写和小写组合在一起:A、a、B、b、C、c 等,而不是 A、B、C、a、b、c。因此,假设大写在前(哪个先到取决于您使用的是排序规则SQL_还是非SQL_排序规则),则一系列[A-C]等于[AaBbC],如果小写在前,则相同范围等于[AbBcC]。在这两种情况下,小写的“b”都在该范围内。

但是,这种方法主要适用于确保您搜索的数据中只有 26 个美国英语字符,并且在大多数其他语言中永远找不到任何其他字符的情况下,尤其是那些带有重音符号/变音符号的字符。鉴于您的数据包含姓名,您无法保证即使在美国工作,因为来自其他地方(或祖先来自其他地方)的人居住在这里,或者即使他们居住在世界任何地方也注册了此处托管的服务。

例如,在上面的示例代码中添加以下两个测试用例:

INSERT INTO @SampleData (Name) VALUES (N'Mr ÜLala Jones');
INSERT INTO @SampleData (Name) VALUES (N'Mr üLala Jones');
Run Code Online (Sandbox Code Playgroud)

再次运行代码不会产生新的结果。添加的第二行使用小写字母,ü因此无论如何都不应该匹配。但是,第一行使用大写Ü所以它应该匹配,但不能,除非我们要么使整理accent-敏感(即Latin1_General_100_CS_AI)或添加Ü字符到两个字符范围LIKE条款(例如[ABCDEFGHIJKLMNOPQRSTUVWXYZÜ])。

在进行比较accent-敏感寻找任何两个大写字母时,紧靠着对方,但没有比较实际的话时可能是一个选项。添加额外的字母(带重音的字母)仅在使用VARCHAR(即扩展 ASCII)数据时有效。如果是这种情况,那么如果您添加了排序规则指定的代码页中可用的所有其他大写字母,则可以使此方法起作用。要找到它,您可以执行以下操作:

SELECT COLLATIONPROPERTY('Latin1_General_100_CI_AS', 'CodePage');
-- 1252
Run Code Online (Sandbox Code Playgroud)

根据代码页 1252的维基百科页面,还有 32 个额外的大写字母,它们是(我将其作为标题,以便更容易看到字符):

Š Ž ÀÁÂÃÄÅ Æ Ç ÈÉÊË ÌÍÎÏ Ð Ñ ÒÓÔÕÖ Ø ÙÚÛÜ Ý Þ

结果将是一个LIKE子句单字符规范:

[ABCDEFGHIJKLMNOPQRSTUVWXYZŠŽÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ]

但该特定字符集仅适用于代码页 1252。如果您的排序规则使用另一个代码页,则该组额外字符很可能会有所不同,即使存在一些重叠。


另一方面,如果您使用 Unicode / NVARCHARdata,那么即使添加这些额外的字符也不起作用。Unicode 不仅可以表示所有带重音的字符(与任何单独的代码页不同),它还允许通过以常规的非带重音的字符(例如“U”)开头,然后向其添加一个或多个重音来构建带重音的字符。这些重音被称为“组合字符”,因为它们只能与某些基本字符组合使用(要更详细地了解组合字符,请参阅我对如何剥离希伯来语重音标记的回答)。上面的注释#3 中显示了一个示例(例如PRINT N'U' + NCHAR(0x0308); -- U?)。

如果我们现在将以下两个测试用例添加到上面的示例代码中:

INSERT INTO @SampleData (Name) VALUES (N'Mr U' + NCHAR(0x0308) + N'Lala Jonez');
INSERT INTO @SampleData (Name) VALUES (N'Mr U' + NCHAR(0x0308) + NCHAR(0x0309) +
                                     NCHAR(0x0308) + N'Lala Jonezzz');
Run Code Online (Sandbox Code Playgroud)

并重新运行测试,这些新条目将不会出现,至少在使用LIKE仅指定美国-英语字母表的 26 个字母的原始定义时不会出现。如果我们添加了Ü字符,那么第一个新条目就会出现,因为U+ 组合字符等同于相同的东西。那么,像我在上面的VARCHAR例子中展示的那样添加剩余的字符是否可行?那是 32 个额外的字符。有多少额外的字符NVARCHAR?大约1200 ;-)。嗯不,不可行。

如果您确实添加了所有 1200 个额外的大写字母,那会起作用吗?不,因为仍然可能有更多的大写字母根本没有“预先组合”的等价物。例如,第二个新条目将不会显示,因为LIKE定义中没有等效字符。使这项工作的唯一方法是不仅应在所有1200次的额外资本补充,而且使用accent-敏感的排序规则。这似乎仍然不太可行。而且它仍然只能在有限的情况下工作,例如这种情况(寻找任何大写字母,而不是特定大写字母)。

不幸的是,T-SQL 没有能力处理这个问题。

但幸运的是,有一种方法可以实现这一点:正则表达式(通常简称为 RegEx)。RegEx 允许指定字符的“类”(例如\s=“任何空白字符”),并且一个类 -- \p{property}-- 将根据 Unicode 字符的某些属性匹配字符。以下两个类规范将允许我们获得任何大写字母,即使它今天不存在:

  • \p{Lu} = 具有小写等效项的大写字母
  • \p{M} = 组合标记

我们可以使用它们来提出以下 RegEx 模式:

(?:\p{Lu}\p{M}*){2}
Run Code Online (Sandbox Code Playgroud)

为那些不熟悉 RegEx 语法的人翻译:

  • ()组内的一切是可以被重复或以后引用单个图案
  • ?:告诉正则表达式引擎没有保存该分组的任何比赛。我们不会使用任何匹配项,因此不将其存储在内存中会更有效。
  • *指示,应该有0个或多个的前一个项目的匹配(即,\p{M})。这将匹配任意数量的变音符号,或者不匹配。
  • \p{Lu}\p{M}*完全表示匹配任何单个大写字母本身,或者后面跟着任意数量的变音符号。这将匹配,例如:UÜU+NCHAR(0x0308)等。
  • {2}表示的匹配将包括正好2发生前述项目中的(即,(?:\p{Lu}\p{M}*))。如果没有这部分,它只会匹配一个大写字母,而不是 2。这就是我们需要括号的原因:将大写字母和零个或多个组合标记组合在一起成为一个子模式可以重复。

唯一需要处理的部分是 SQL Server 没有任何对正则表达式的内置支持。为此,我们求助于 SQLCLR,它允许我们使用 .NET 中存在的 RegEx 功能。如果您还没有 SQLCLR RegEx 函数,Interwebs 上有很多代码示例(请小心,因为其中一些非常好,但很多都非常糟糕),或者您可以下载SQL#的免费版本,这是一个预SQLCLR 函数(我创建的)的编译库,其中包含多个 RegEx 函数,包括以下示例代码中使用的函数:

(?:\p{Lu}\p{M}*){2}
Run Code Online (Sandbox Code Playgroud)

再次针对测试数据运行将带回所有预期的条目,包括带有 3 个变音标记的最后一个,它甚至不是任何语言中的字符(至少现在不是)。将 RegEx 与这种模式一起使用将始终适用于所有已知语言(嗯,有大写的语言,因为有些没有大写)。