ORDER BY和字母数字混合串的比较

db2*_*db2 9 sql-server collation sql-server-2012 sorting natural-sort

我们需要对通常是需要“自然”排序的数字和字母混合字符串的值进行一些报告。诸如“P7B18”或“P12B3”之类的东西。@字符串将主要是字母序列然后数字交替。但是,这些段的数量和每个段的长度可能会有所不同。

我们希望这些数字部分按数字顺序排序。显然,如果我直接用 处理这些字符串值ORDER BY,那么“P12B3”将在“P7B18”之前出现,因为“P1”早于“P7”,但我希望相反,因为“P7”自然在前面“P12”。

我还希望能够进行范围比较,例如@bin < 'P13S6'或类似的。我不必处理浮点数或负数;这些将严格是我们正在处理的非负整数。字符串长度和段数可能是任意的,没有固定的上限。

在我们的例子中,字符串大小写并不重要,但如果有一种方法可以以排序方式感知方式做到这一点,其他人可能会发现这很有用。所有这一切中最丑陋的部分是我希望能够在WHERE子句中进行排序和范围过滤。

如果我在 C# 中执行此操作,这将是一项非常简单的任务:进行一些解析以将 alpha 与数字分开,实现 IComparable,您基本上就完成了。当然,SQL Server 似乎没有提供任何类似的功能,至少就我所知。

任何人都知道使这项工作有什么好的技巧吗?是否有一些很少公开的能力来创建实现 IComparable 的自定义 CLR 类型并使其按预期运行?我也不反对 Stupid XML Tricks(另请参阅:list concatenation),而且我在服务器上也提供了 CLR 正则表达式匹配/提取/替换包装函数。

编辑: 作为一个更详细的例子,我希望数据表现得像这样。

SELECT bin FROM bins ORDER BY bin

bin
--------------------
M7R16L
P8RF6JJ
P16B5
PR7S19
PR7S19L
S2F3
S12F0
Run Code Online (Sandbox Code Playgroud)

即将字符串分成所有字母或所有数字的标记,并分别按字母或数字对它们进行排序,最左边的标记是最重要的排序项。就像我提到的,如果您实现 IComparable,在 .NET 中是小菜一碟,但我不知道如何(或是否)您可以在 SQL Server 中执行此类操作。这肯定不是我在 10 年左右的工作中遇到过的事情。

Sol*_*zky 8

想要一种将字符串中的数字作为实际数字进行排序的明智、有效的方法吗?考虑投票支持我的 Microsoft Connect 建议:支持“自然排序”/DIGITSASNUMBERS 作为整理选项


没有简单的内置方法可以做到这一点,但有一种可能性:

通过将字符串重新格式化为固定长度的段来规范化字符串:

  • 创建一个类型为 的排序列VARCHAR(50) COLLATE Latin1_General_100_BIN2。50 的最大长度可能需要根据段的最大数量及其潜在的最大长度进行调整。
  • 虽然规范化可以在应用程序层更有效地完成,但使用 T-SQL UDF 在数据库中处理此问题将允许将标量 UDF 放入AFTER [or FOR] INSERT, UPDATE触发器中,这样您就可以保证正确设置所有记录的值,即使是那些通过即席查询等传入。当然,该标量 UDF 也可以通过 SQLCLR 处理,但需要对其进行测试以确定哪个实际上更有效。**
  • UDF(无论是在 T-SQL 还是 SQLCLR 中)应该:
    • 通过读取每个字符并在类型从字母转换为数字或数字转换为字母时停止来处理未知数量的段。
    • 对于每个段,它应该返回一个固定长度的字符串,该字符串设置为任何段的最大可能字符/数字(或者 max + 1 或 2 以考虑未来的增长)。
    • Alpha 段应左对齐并右填充空格。
    • 数字段应该右对齐并用零填充左。
    • 如果字母字符可以混合大小写但排序需要不区分大小写,则将该UPPER()函数应用于所有段的最终结果(以便它只需要执行一次而不是每个段)。考虑到排序列的二进制排序规则,这将允许正确排序。
  • AFTER INSERT, UPDATE在表上创建一个触发器,调用 UDF 来设置排序列。为了提高性能,使用UPDATE()函数判断这个code列是否在语句的SET子句中UPDATERETURN如果为false则简单),然后将code列上的INSERTEDDELETED伪表join起来,只处理code值有变化的行. 请务必指定COLLATE Latin1_General_100_BIN2该 JOIN 条件,以确保确定是否有更改的准确性。
  • 在新的排序列上创建索引。

例子:

P7B18   -> "P     000007B     000018"
P12B3   -> "P     000012B     000003"
P12B3C8 -> "P     000012B     000003C     000008"
Run Code Online (Sandbox Code Playgroud)

在这种方法中,您可以通过以下方式进行排序:

ORDER BY tbl.SortColumn
Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式进行范围过滤:

WHERE tbl.SortColumn BETWEEN dbo.MyUDF('P7B18') AND dbo.MyUDF('P12B3')
Run Code Online (Sandbox Code Playgroud)

或者:

DECLARE @RangeStart VARCHAR(50),
        @RangeEnd VARCHAR(50);
SELECT @RangeStart = dbo.MyUDF('P7B18'),
       @RangeEnd = dbo.MyUDF('P12B3');

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd
Run Code Online (Sandbox Code Playgroud)

无论是ORDER BYWHERE过滤器应用于定义的二进制排序SortColumn由于排序规则的优先顺序。

仍将在原始值列上进行相等比较。


其他想法:

  • 使用 SQLCLR UDT。这可能会奏效,但与上述方法相比,尚不清楚它是否会带来净收益。

    是的,SQLCLR UDT 可以使用自定义算法覆盖其比较运算符。这可以处理将值与另一个已经是相同自定义类型的值或需要隐式转换的值进行比较的情况。这应该WHERE条件下处理范围过滤器。

    关于将 UDT 排序为常规列类型(不是计算列),这仅在 UDT 是“字节排序”时才有可能。“字节排序”意味着 UDT 的二进制表示(可以在 UDT 中定义)自然地以适当的顺序排序。假设二进制表示的处理类似于上述针对 VARCHAR(50) 列的方法进行处理,该列具有填充的固定长度段,这将是合格的。或者,如果不容易确保以正确的方式自然地对二进制表示进行排序,您可以公开 UDT 的一个方法或属性,该方法或属性输出一个正确排序的值,然后PERSISTED在其上创建一个计算列方法或属性。该方法需要确定性并标记为IsDeterministic = true

    这种方法的好处是:

    • 不需要“原始值”字段。
    • 无需调用 UDF 来插入数据或比较值。假设ParseUDT的方法接收P7B18值并对其进行转换,那么您应该能够简单地将值自然地插入为P7B18. 并且使用 UDT 中设置的隐式转换方法,WHERE 条件还允许仅使用 P7B18`。

    这种方法的后果是:

    • 如果使用字节排序的 UDT 作为列数据类型,只需选择该字段将返回二进制表示。或者,如果PERSISTED在 UDT 的属性或方法上使用计算列,则您将获得该属性或方法返回的表示。如果需要原始P7B18值,则需要调用 UDT 的方法或属性,该方法或属性已编码以返回该表示。由于ToString无论如何您都必须覆盖该方法,因此这是提供此功能的一个很好的候选者。
    • 目前尚不清楚(至少现在对我而言,因为我还没有测试过这部分)对二进制表示进行任何更改是多么容易/困难。更改存储的可排序表示可能需要删除并重新添加字段。此外,如果以任何一种方式使用,删除包含 UDT 的程序集都会失败,因此您需要确保除了此 UDT 之外,程序集中没有任何其他内容。您可以ALTER ASSEMBLY替换定义,但对此有一些限制。

      另一方面,该VARCHAR()字段是与算法断开连接的数据,因此它只需要更新列。如果有数千万行(或更多),则可以通过批处理方式完成。

  • 实现实际上允许进行这种字母数字排序的ICU库。虽然功能强大,但该库仅提供两种语言:C/C++ 和 Java。这意味着您可能需要进行一些调整才能使其在 Visual C++ 中工作,或者使用IKVM将 Java 代码转换为 MSIL 的可能性很小。该站点上链接了一个或两个 .NET 辅助项目,它们提供了一个可以在托管代码中访问的 COM 接口,但我相信它们已经有一段时间没有更新了,我也没有尝试过。最好的办法是在应用层处理这个,目的是生成排序键。然后将排序键保存到新的排序列中。

    这可能不是最实用的方法。不过,这样的能力存在还是很爽的。我在以下答案中提供了对此示例的更详细的演练:

    是否有排序规则按以下顺序 1,2,3,6,10,10A,10B,11 对以下字符串进行排序?

    但是在那个问题中处理的模式要简单一些。有关显示此问题中处理的模式类型也有效的示例,请转到以下页面:

    ICU 整理演示

    在“设置”下,将“数字”选项设置为“开”,所有其他选项都应设置为“默认”。接下来,在“排序”按钮的右侧,取消选中“差异强度”选项并选中“排序键”选项。然后将“输入”文本区域中的项目列表替换为以下列表:

    P12B22
    P7B18
    P12B3
    as456456hgjg6786867
    P7Bb19
    P7BA19
    P7BB19
    P007B18
    P7Bb20
    P7Bb19z23
    
    Run Code Online (Sandbox Code Playgroud)

    单击“排序”按钮。“输出”文本区域应显示以下内容:

    as456456hgjg6786867
        29 4D 0F 7A EA C8 37 35 3B 35 0F 84 17 A7 0F 93 90 , 0D , , 0D .
    P7B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P007B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P7BA19
        47 0F 09 2B 29 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19
        47 0F 09 2B 2B 0F 15 , 09 , FD F2 , DC C5 DC 06 .
    P7BB19
        47 0F 09 2B 2B 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19z23
        47 0F 09 2B 2B 0F 15 5B 0F 19 , 0B , FD F4 , DC C5 DC 08 .
    P7Bb20
        47 0F 09 2B 2B 0F 16 , 09 , FD F2 , DC C5 DC 06 .
    P12B3
        47 0F 0E 2B 0F 05 , 08 , FD F1 , DC C5 DC 05 .
    P12B22
        47 0F 0E 2B 0F 18 , 08 , FD F1 , DC C5 DC 05 .
    
    Run Code Online (Sandbox Code Playgroud)

    请注意,排序键是多个字段的结构,以逗号分隔。每个字段都需要独立排序,因此如果需要在 SQL Server 中实现它,这就提出了另一个需要解决的小问题。


**如果对使用用户定义函数的性能有任何顾虑,请注意所提议的方法对它们的使用最少。事实上,存储规范化值的主要原因是避免为每个查询的每一行调用一个 UDF。在主要方法中,UDF 用于设置 的值SortColumn,这仅在触发器上INSERTUPDATE通过触发器完成。选择值比插入和更新更常见,并且某些值永远不会更新。对于在子句SELECT中使用SortColumnfor 范围过滤器的每个查询,WHERE每个 range_start 和 range_end 值只需要一次 UDF 来获取规范化值;UDF 不是按行调用的。

关于UDT,用法其实和标量UDF是一样的。意思是,插入和更新将每行调用一次规范化方法来设置值。然后,对于范围过滤器中的每个 range_start 和 range_value,每个查询都会调用一次规范化方法,而不是每行。

支持完全在 SQLCLR UDF 中处理规范化的一点是,鉴于它不进行任何数据访问并且是确定性的,如果它被标记为IsDeterministic = true,那么它可以参与并行计划(这可能有助于INSERTUPDATE操作),而T-SQL UDF 将阻止使用并行计划。