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


9

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

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

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

在我们的案例中,字符串大小写并不重要,尽管如果有一种方法可以按排序规则识别的方式执行此操作,那么其他人可能会觉得有用。所有这一切中最丑陋的部分是我希望能够在WHERE子句中同时进行排序和范围过滤。

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

有人知道有什么好办法可以使这项工作吗?是否有一些鲜为人知的功能来创建实现IComparable的自定义CLR类型,并使它的行为符合预期?我也不反对愚蠢的XML技巧(另请参阅:列表串联),并且服务器上也提供了CLR regex匹配/提取/替换包装器功能。

编辑: 作为一个更详细的示例,我希望数据表现出这样的效果。

SELECT bin FROM bins ORDER BY bin

bin
--------------------
M7R16L
P8RF6JJ
P16B5
PR7S19
PR7S19L
S2F3
S12F0

例如,将字符串分成所有字母或所有数字的记号,并分别按字母或数字对它们进行排序,最左边的记号是最重要的排序术语。就像我提到的那样,如果实现IComparable,.NET无疑是小菜一碟,但是我不知道如何(或是否)可以在SQL Server中实现这种功能。在使用它的10年左右的时间里,这肯定不是我遇到过的事情。


您可以使用某种索引的计算列来执行此操作,将字符串转换为整数。所以P7B12有可能成为P 07 B 12,然后(通过ASCII) 80 07 65 12,所以80076512
Philᵀᴹ

我建议您创建一个计算列,该列将每个数字分量填充为较大的长度(即10个零)。由于格式相当随意,因此您需要一个很大的内联表达式,但这是可行的。然后,您可以根据需要在该列上进行索引/排序。
Nick.McDermaid

请查看我刚刚添加到答案顶部的链接:)
所罗门·鲁兹基

1
@srutzky尼斯,我投票赞成。
db2

嗨,db2:由于Microsoft从Connect移到UserVoice,并且没有完全保留投票数(他们在评论中表示怀疑,但不确定他们对此有何评论),因此您可能需要重新投票:支持“自然排序” / DIGITSASNUMBERS作为排序规则选项。谢谢!
所罗门·鲁兹基

Answers:


8

是否需要一种明智,有效的方法来将字符串中的数字排序为实际数字?考虑投票支持我的Microsoft Connect建议:支持“自然排序” / DIGITSASNUMBERS作为排序规则选项


没有简单的内置方法可以执行此操作,但是有一种可能:

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

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

例:

P7B18   -> "P     000007B     000018"
P12B3   -> "P     000012B     000003"
P12B3C8 -> "P     000012B     000003C     000008"

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

ORDER BY tbl.SortColumn

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

WHERE tbl.SortColumn BETWEEN dbo.MyUDF('P7B18') AND dbo.MyUDF('P12B3')

要么:

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

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd

无论是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

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

    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 .

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


**如果对使用用户定义的功能的性能存在任何担忧,请注意,建议的方法对它们的使用最少。实际上,存储归一化值的主要原因是避免在每个查询的每一行中调用UDF。在主要方法中,UDF用于设置的值SortColumn,并且仅在触发器上INSERTUPDATE通过触发器来完成。选择值比插入和更新更为常见,并且某些值永远不会更新。对于子句SELECT中使用SortColumn范围过滤器的每个查询,WHERE每个range_start和range_end值仅需要一次UDF即可获得标准化值;UDF不称为每行。

关于UDT,用法实际上与标量UDF相同。意思是,插入和更新将每行调用一次标准化方法以设置值。然后,将对范围过滤器中每个range_start和range_value的每个查询调用一次标准化方法,但不会按行调用一次。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.