Microsoft SQL 2005中的自然(人类字母数字)排序

时间:2008-08-29 15:55:10

标签: sql-server sql-server-2005 sorting natural-sort

我们有一个大型数据库,我们有数据库端分页。这很快,在几分之一秒内从数百万条记录中返回50行的页面。

用户可以定义自己的排序,基本上选择要排序的列。列是动态的 - 有些具有数值,有些日期和一些文本。

虽然大多数按预期文本排序是愚蠢的。嗯,我说愚蠢,它对计算机有意义,但让用户感到沮丧。

例如,按字符串记录id排序可以得到如下内容:

rec1
rec10
rec14
rec2
rec20
rec3
rec4

......等等。

我希望这个考虑到数字,所以:

rec1
rec2
rec3
rec4
rec10
rec14
rec20

我无法控制输入(否则我只是在前导000中格式化)而且我不能依赖单一格式 - 有些像“{alpha code} - {dept code} - {rec id }”。

我知道在C#中有几种方法可以做到这一点,但是无法下拉所有记录来对它们进行排序,因为这样做会很慢。

有没有人知道在Sql server中快速应用自然排序的方法?


我们正在使用:

ROW_NUMBER() over (order by {field name} asc)

然后我们就这样分页。

我们可以添加触发器,但我们不会。他们所有的输入都是参数化的,但我无法改变格式 - 如果他们输入“rec2”和“rec10”,他们希望它们就像那样,以自然的顺序返回。


我们的有效用户输入遵循不同客户的不同格式。

有人可能会去rec1,rec2,rec3,... rec100,rec101

而另一个可能会去:grp1rec1,grp1rec2,... grp20rec300,grp20rec301

当我说我们无法控制输入时,我的意思是我们不能强迫用户更改这些标准 - 它们有一个像grp1rec1的值,我不能将其重新格式化为grp01rec001,因为这会改变使用的东西用于查找和链接到外部系统。

这些格式变化很大,但通常是字母和数字的混合。

在C#中对这些进行排序很简单 - 只需将其分解为{ "grp", 20, "rec", 301 },然后依次比较序列值。

然而,可能有数百万条记录并且数据被分页,我需要在SQL服务器上进行排序。

SQL服务器按值排序,而不是比较 - 在​​C#中我可以将值拆分为比较,但在SQL中我需要一些逻辑(非常快)获得一致排序的单个值。

@moebius - 您的答案可能有效,但是为所有这些文本值添加排序键确实感觉是一种丑陋的妥协。

13 个答案:

答案 0 :(得分:43)

order by LEN(value), value

不完美,但在很多情况下运作良好。

答案 1 :(得分:29)

我看到的大多数基于SQL的解决方案在数据足够复杂时会中断(例如,其中包含多个或两个数字)。最初我尝试在T-SQL中实现符合我要求的NaturalSort函数(除其他外,在字符串中处理任意数量的数字),但性能方式太慢。

最终,我在C#中编写了一个标量CLR函数以允许自然排序,即使使用未经优化的代码,从SQL Server调用它的性能也非常快。它具有以下特点:

  • 将正确排序前1,000个字符(在代码中轻松修改或作为参数)
  • 正确排序小数,因此123.333来自123.45
  • 因为上述原因,可能无法正确排序IP地址等内容;如果您希望有不同的行为,请修改代码
  • 支持对其中包含任意数量的数字的字符串进行排序
  • 将正确排序长度最多为25位的数字(可在代码中轻松修改或作为参数)

代码在这里:

using System;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public class UDF
{
    [SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic=true)]
    public static SqlString Naturalize(string val)
    {
        if (String.IsNullOrEmpty(val))
            return val;

        while(val.Contains("  "))
            val = val.Replace("  ", " ");

        const int maxLength = 1000;
        const int padLength = 25;

        bool inNumber = false;
        bool isDecimal = false;
        int numStart = 0;
        int numLength = 0;
        int length = val.Length < maxLength ? val.Length : maxLength;

        //TODO: optimize this so that we exit for loop once sb.ToString() >= maxLength
        var sb = new StringBuilder();
        for (var i = 0; i < length; i++)
        {
            int charCode = (int)val[i];
            if (charCode >= 48 && charCode <= 57)
            {
                if (!inNumber)
                {
                    numStart = i;
                    numLength = 1;
                    inNumber = true;
                    continue;
                }
                numLength++;
                continue;
            }
            if (inNumber)
            {
                sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));
                inNumber = false;
            }
            isDecimal = (charCode == 46);
            sb.Append(val[i]);
        }
        if (inNumber)
            sb.Append(PadNumber(val.Substring(numStart, numLength), isDecimal, padLength));

        var ret = sb.ToString();
        if (ret.Length > maxLength)
            return ret.Substring(0, maxLength);

        return ret;
    }

    static string PadNumber(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}

要注册这个以便您可以从SQL Server调用它,请在查询分析器中运行以下命令:

CREATE ASSEMBLY SqlServerClr FROM 'SqlServerClr.dll' --put the full path to DLL here
go
CREATE FUNCTION Naturalize(@val as nvarchar(max)) RETURNS nvarchar(1000) 
EXTERNAL NAME SqlServerClr.UDF.Naturalize
go

然后,您可以这样使用它:

select *
from MyTable
order by dbo.Naturalize(MyTextField)

注意:如果在SQL Server中出现错误,则禁用.NET Framework中的用户代码执行。启用“clr enabled”配置选项。,按照说明here启用它。在这样做之前,请确保考虑安全隐患。如果您不是数据库管理员,请确保在对服务器配置进行任何更改之前与管理员讨论此问题。

注2 :此代码不能正确支持国际化(例如,假设小数点是“。”,未针对速度进行优化等。欢迎提出改进建议!

修改:将该功能重命名为 Naturalize 而非 NaturalSort ,因为它不会进行任何实际排序。

答案 2 :(得分:14)

我知道这是一个古老的问题,但我刚刚遇到它,因为它没有得到一个接受的答案。

我总是使用类似的方法:

SELECT [Column] FROM [Table]
ORDER BY RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))), 1000)

唯一常见的问题是,如果您的列不会转换为VARCHAR(MAX),或者LEN([Column])&gt; 1000(但如果你愿意,你可以将1000改为其他东西),但你可以根据需要使用这个粗略的想法。

此外,这比正常的ORDER BY [Column]表现得差得多,但它确实可以为您提供OP中要求的结果。

编辑:只是为了进一步澄清,如果你有11.151.5之类的小数值,上面的内容将不起作用(它们将排序为{{1}因为这不是OP中要求的内容,但可以通过以下方式轻松完成:

{1, 1.5, 1.15}

结果:SELECT [Column] FROM [Table] ORDER BY REPLACE(RIGHT(REPLICATE('0', 1000) + LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX)))) + REPLICATE('0', 100 - CHARINDEX('.', REVERSE(LTRIM(RTRIM(CAST([Column] AS VARCHAR(MAX))))), 1)), 1000), '.', '0')

仍然完全在SQL中。这不会对IP地址进行排序,因为您现在正在进入非常具体的数字组合,而不是简单的文本+数字。

答案 3 :(得分:6)

RedFilter's answer非常适用于索引不重要的合理大小的数据集,但是如果您需要索引,则需要进行一些调整。

首先,将该功能标记为不进行任何数据访问,并且具有确定性和准确性:

[SqlFunction(DataAccess = DataAccessKind.None,
                          SystemDataAccess = SystemDataAccessKind.None,
                          IsDeterministic = true, IsPrecise = true)]

接下来,MSSQL对索引键大小有900字节的限制,因此如果自然值是索引中的唯一值,则它必须最多为450个字符。如果索引包含多个列,则返回值必须更小。两个变化:

CREATE FUNCTION Naturalize(@str AS nvarchar(max)) RETURNS nvarchar(450)
    EXTERNAL NAME ClrExtensions.Util.Naturalize

并在C#代码中:

const int maxLength = 450;

最后,您需要向表中添加一个计算列,并且必须将其保留(因为MSSQL无法证明Naturalize具有确定性和精确性),这意味着归化值实际存储在表中但仍然会自动维护:

ALTER TABLE YourTable ADD nameNaturalized AS dbo.Naturalize(name) PERSISTED

您现在可以创建索引了!

CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized)

我还对RedFilter的代码进行了一些更改:使用字符清晰,将重复空间删除合并到主循环中,一旦结果超过限制就退出,设置最大长度substring等。结果如下:

using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

public static class Util
{
    [SqlFunction(DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = true, IsPrecise = true)]
    public static SqlString Naturalize(string str)
    {
        if (string.IsNullOrEmpty(str))
            return str;

        const int maxLength = 450;
        const int padLength = 15;

        bool isDecimal = false;
        bool wasSpace = false;
        int numStart = 0;
        int numLength = 0;

        var sb = new StringBuilder();
        for (var i = 0; i < str.Length; i++)
        {
            char c = str[i];
            if (c >= '0' && c <= '9')
            {
                if (numLength == 0)
                    numStart = i;
                numLength++;
            }
            else
            {
                if (numLength > 0)
                {
                    sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));
                    numLength = 0;
                }
                if (c != ' ' || !wasSpace)
                    sb.Append(c);
                isDecimal = c == '.';
                if (sb.Length > maxLength)
                    break;
            }
            wasSpace = c == ' ';
        }
        if (numLength > 0)
            sb.Append(pad(str.Substring(numStart, numLength), isDecimal, padLength));

        if (sb.Length > maxLength)
            sb.Length = maxLength;
        return sb.ToString();
    }

    private static string pad(string num, bool isDecimal, int padLength)
    {
        return isDecimal ? num.PadRight(padLength, '0') : num.PadLeft(padLength, '0');
    }
}

答案 4 :(得分:5)

我知道此时此刻有点陈旧,但在寻找更好的解决方案时,我遇到了这个问题。我目前正在使用一个功能来订购。它适用于我的目的是排序以混合字母数字命名的记录('项目1','项目10','项目2'等)

CREATE FUNCTION [dbo].[fnMixSort]
(
    @ColValue NVARCHAR(255)
)
RETURNS NVARCHAR(1000)
AS

BEGIN
    DECLARE @p1 NVARCHAR(255),
        @p2 NVARCHAR(255),
        @p3 NVARCHAR(255),
        @p4 NVARCHAR(255),
        @Index TINYINT

    IF @ColValue LIKE '[a-z]%'
        SELECT  @Index = PATINDEX('%[0-9]%', @ColValue),
            @p1 = LEFT(CASE WHEN @Index = 0 THEN @ColValue ELSE LEFT(@ColValue, @Index - 1) END + REPLICATE(' ', 255), 255),
            @ColValue = CASE WHEN @Index = 0 THEN '' ELSE SUBSTRING(@ColValue, @Index, 255) END
    ELSE
        SELECT  @p1 = REPLICATE(' ', 255)

    SELECT  @Index = PATINDEX('%[^0-9]%', @ColValue)

    IF @Index = 0
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255),
            @ColValue = ''
    ELSE
        SELECT  @p2 = RIGHT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    SELECT  @Index = PATINDEX('%[0-9,a-z]%', @ColValue)

    IF @Index = 0
        SELECT  @p3 = REPLICATE(' ', 255)
    ELSE
        SELECT  @p3 = LEFT(REPLICATE(' ', 255) + LEFT(@ColValue, @Index - 1), 255),
            @ColValue = SUBSTRING(@ColValue, @Index, 255)

    IF PATINDEX('%[^0-9]%', @ColValue) = 0
        SELECT  @p4 = RIGHT(REPLICATE(' ', 255) + @ColValue, 255)
    ELSE
        SELECT  @p4 = LEFT(@ColValue + REPLICATE(' ', 255), 255)

    RETURN  @p1 + @p2 + @p3 + @p4

END

然后致电

select item_name from my_table order by fnMixSort(item_name)

简单的数据读取很容易使处理时间增加三倍,因此可能不是完美的解决方案。

答案 5 :(得分:5)

这是为SQL 2000编写的解决方案。对于较新的SQL版本,它可能会得到改进。

/**
 * Returns a string formatted for natural sorting. This function is very useful when having to sort alpha-numeric strings.
 *
 * @author Alexandre Potvin Latreille (plalx)
 * @param {nvarchar(4000)} string The formatted string.
 * @param {int} numberLength The length each number should have (including padding). This should be the length of the longest number. Defaults to 10.
 * @param {char(50)} sameOrderChars A list of characters that should have the same order. Ex: '.-/'. Defaults to empty string.
 *
 * @return {nvarchar(4000)} A string for natural sorting.
 * Example of use: 
 * 
 *      SELECT Name FROM TableA ORDER BY Name
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                        ID  Name
 *  1.  A1.                         1.  A1-1.       
 *  2.  A1-1.                       2.  A1.
 *  3.  R1             -->          3.  R1
 *  4.  R11                         4.  R11
 *  5.  R2                          5.  R2
 *
 *  
 *  As we can see, humans would expect A1., A1-1., R1, R2, R11 but that's not how SQL is sorting it.
 *  We can use this function to fix this.
 *
 *      SELECT Name FROM TableA ORDER BY dbo.udf_NaturalSortFormat(Name, default, '.-')
 *  TableA (unordered)              TableA (ordered)
 *  ------------                    ------------
 *  ID  Name                        ID  Name
 *  1.  A1.                         1.  A1.     
 *  2.  A1-1.                       2.  A1-1.
 *  3.  R1              -->         3.  R1
 *  4.  R11                         4.  R2
 *  5.  R2                          5.  R11
 */
ALTER FUNCTION [dbo].[udf_NaturalSortFormat](
    @string nvarchar(4000),
    @numberLength int = 10,
    @sameOrderChars char(50) = ''
)
RETURNS varchar(4000)
AS
BEGIN
    DECLARE @sortString varchar(4000),
        @numStartIndex int,
        @numEndIndex int,
        @padLength int,
        @totalPadLength int,
        @i int,
        @sameOrderCharsLen int;

    SELECT 
        @totalPadLength = 0,
        @string = RTRIM(LTRIM(@string)),
        @sortString = @string,
        @numStartIndex = PATINDEX('%[0-9]%', @string),
        @numEndIndex = 0,
        @i = 1,
        @sameOrderCharsLen = LEN(@sameOrderChars);

    -- Replace all char that have the same order by a space.
    WHILE (@i <= @sameOrderCharsLen)
    BEGIN
        SET @sortString = REPLACE(@sortString, SUBSTRING(@sameOrderChars, @i, 1), ' ');
        SET @i = @i + 1;
    END

    -- Pad numbers with zeros.
    WHILE (@numStartIndex <> 0)
    BEGIN
        SET @numStartIndex = @numStartIndex + @numEndIndex;
        SET @numEndIndex = @numStartIndex;

        WHILE(PATINDEX('[0-9]', SUBSTRING(@string, @numEndIndex, 1)) = 1)
        BEGIN
            SET @numEndIndex = @numEndIndex + 1;
        END

        SET @numEndIndex = @numEndIndex - 1;

        SET @padLength = @numberLength - (@numEndIndex + 1 - @numStartIndex);

        IF @padLength < 0
        BEGIN
            SET @padLength = 0;
        END

        SET @sortString = STUFF(
            @sortString,
            @numStartIndex + @totalPadLength,
            0,
            REPLICATE('0', @padLength)
        );

        SET @totalPadLength = @totalPadLength + @padLength;
        SET @numStartIndex = PATINDEX('%[0-9]%', RIGHT(@string, LEN(@string) - @numEndIndex));
    END

    RETURN @sortString;
END

答案 6 :(得分:3)

对于以下varchar数据:

BR1
BR2
External Location
IR1
IR2
IR3
IR4
IR5
IR6
IR7
IR8
IR9
IR10
IR11
IR12
IR13
IR14
IR16
IR17
IR15
VCR

这对我来说效果最好:

ORDER BY substring(fieldName, 1, 1), LEN(fieldName)

答案 7 :(得分:3)

这是我喜欢的其他解决方案: http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

这不是Microsoft SQL,但是当我在寻找Postgres的解决方案时我最终到了这里,我想在这里添加这个可以帮助其他人。

答案 8 :(得分:1)

如果您无法从数据库加载数据以使用C#进行排序,那么我确信您会对在数据库中以编程方式执行此操作的任何方法感到失望。当服务器要进行排序时,它必须像每次一样计算“感知”顺序。

我建议您在首次插入数据时添加一个额外的列来使用某种C#方法存储预处理的可排序字符串。例如,您可能会尝试将数字转换为固定宽度范围,因此“xyz1”将变为“xyz00000001”。然后,您可以使用正常的SQL Server排序。

冒着烦恼的风险,我写了一篇CodeProject文章来实现CodingHorror文章中提出的问题。随意steal from my code

答案 9 :(得分:0)

您可以使用以下代码解决问题:

Select *, 
    substring(Cote,1,len(Cote) - Len(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1)))alpha,
    CAST(RIGHT(Cote, LEN(Cote) - PATINDEX('%[0-9]%', Cote)+1) AS INT)intv 
FROM Documents 
   left outer join Sites ON Sites.IDSite = Documents.IDSite 
Order BY alpha, intv

的问候, rabihkahaleh@hotmail.com

答案 10 :(得分:0)

我刚读过一篇关于这个话题的文章。关键点是:您只需要整数值来排序数据,而'rec'字符串属于UI。您可以将信息拆分为两个字段,例如alpha和num,按alpha和num(单独)排序,然后显示由alpha + num组成的字符串。您可以使用计算列来组成字符串或视图。 希望它有所帮助

答案 11 :(得分:0)

只需按照

排序
ORDER BY 
cast (substring(name,(PATINDEX('%[0-9]%',name)),len(name))as int)

 ##

答案 12 :(得分:-1)

我仍然不明白(可能是因为我的英语不好)。

你可以尝试:

ROW_NUMBER() OVER (ORDER BY dbo.human_sort(field_name) ASC)

但它不适用于数百万条记录。

这就是为什么我建议使用人类价值填充

此外:

  • 内置的T-SQL功能确实如此 微软建议使用缓慢 而是.NET函数。
  • 人类价值是不变的,所以每次都没有计算点数 当查询运行时。