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

我们有一个庞大的数据库,我们有DB侧分页。 这很快,在几秒钟内从数百万条logging中返回一行50页。

用户可以定义自己的sorting,基本上selectsorting的列。 列是dynamic的 – 一些有数字值,一些date和一些文本。

虽然大多数按预期的文本sorting愚蠢的方式。 那么,我说愚蠢,这是有道理的电脑,但挫败用户。

例如,通过stringloggingID进行sorting就像下面这样:

rec1 rec10 rec14 rec2 rec20 rec3 rec4 

…等等。

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

 rec1 rec2 rec3 rec4 rec10 rec14 rec20 

我不能控制input(否则我只是在前面的000s格式),我不能依靠一种格式 – 有些是像“{代码} – {代码} – {rec id}”的东西。

我知道有几种方法可以在C#中执行此操作,但无法将所有logging拆分,因为这样会减慢速度。

有谁知道一种方法来快速应用自然sorting在Sql服务器?


我们正在使用:

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

然后,我们正在分页。

我们可以添加触发器,虽然我们不会。 他们所有的input都是参数化的,但是我不能改变格式 – 如果他们input“rec2”和“rec10”,他们希望他们按照自然顺序返回。


我们有有效的用户input,针对不同的客户使用不同的格式。

可以去rec1,rec2,rec3,… rec100,rec101

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

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

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

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

但是,可能有数百万条logging和数据被分页,我需要在SQL服务器上进行sorting。

SQL服务器按值sorting,而不是比较 – 在​​C#中,我可以将值分割出来进行比较,但在SQL中,我需要一些逻辑(非常快)得到一个一致的sorting值。

@moebius – 你的答案可能会起作用,但是为所有这些文本值添加一个sorting键的确感觉像是一个难看的妥协。

当数据变得足够复杂时,大部分基于SQL的解决scheme都会中断(例如,其中包含多个或两个数字)。 最初,我尝试在T-SQL中实现一个NaturalSort函数,以满足我的要求(其中包括处理string中任意数目的数字),但是性能太慢了。

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

  • 将前1000个左右的字符正确sorting(很容易在代码中修改或作为参数)
  • 正确地对小数进行sorting,所以123.333在123.45之前
  • 由于以上原因,可能不会正确地分类IP地址等东西; 如果你希望不同的行为,修改代码
  • 支持对其中任意数量的数字进行sorting
  • 将正确地对长达25位数字的数字进行sorting(很容易在代码中修改或作为参数)

代码在这里:

 using System; using System.Data.SqlTypes; using System.Text; using Microsoft.SqlServer.Server; public class UDF { [SqlFunction(DataAccess = DataAccessKind.Read)] 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”configuration选项。 ,按照这里的说明启用它。 确保在这样做之前考虑安全影响。 如果您不是数据库pipe理员,请确保在对服务器configuration进行任何更改之前,先与您的pipe理员讨论此问题。

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

编辑:重命名该函数Naturalize而不是NaturalSort ,因为它没有做任何实际的sorting。

 order by LEN(value), value 

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

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

我一直使用类似的方法:

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

这个问题唯一的共同点就是如果你的列不会强制转换为VARCHAR(MAX),或者如果LEN([Column])大于1000(但是如果你愿意的话,你可以把1000更改为别的),但是你可以使用这个粗略的想法,你需要什么。

此外,这是比正常ORDER BY [列]更糟糕的性能,但它给你在OP中要求的结果。

编辑:只是为了进一步澄清,如果你有十进制值,如1.151.5 ,(他们将sorting为{1, 1.5, 1.15} ),这是不会工作,因为这是不是要求在OP,但这可以很容易地完成:

 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') 

结果: {1, 1.15, 1.5}

而且还完全在SQL中。 这不会对IP地址进行sorting,因为您现在正在进入特定的数字组合,而不是简单的文本+数字。

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

首先,将函数标记为不做任何数据访问,并且是确定性的和精确的:

 [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不能certificateNaturalize是确定性的和精确的),这意味着归化值实际上被存储在表中,但是仍然是自动维护的:

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

你现在可以创build索引!

 CREATE INDEX idx_YourTable_n ON YourTable (nameNaturalized) 

我也对RedFilter的代码进行了一些修改:使用字符清晰,将重复空间移除到主循环中,退出一旦结果超过限制,设置最大长度而不用子串等等。结果如下:

 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'); } } 

我知道现在有点老了,但在寻找更好的解决scheme时,我遇到了这个问题。 我目前正在使用一个函数来sorting。 它适用于sorting混合字母数字('项目1','项目10','项目2'等)命名的logging的目的,

 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 '[az]%' 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,az]%', @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) 

它很容易将处理时间缩短三倍,以便读取简单的数据,因此可能不是完美的解决scheme。

这是一个为SQL 2000编写的解决scheme。对于较新的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 

以下是我喜欢的其他解决scheme: http : //www.dreamchain.com/sql-and-alpha-numeric-sort-order/

这不是Microsoft SQL,但是因为当我为Postgres寻找一个解决scheme时,我结束了这里,我认为在这里添加这个可以帮助其他人。

对于以下的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) 

如果您在加载数据库中的数据时遇到问题,需要用C#进行sorting,那么我相信您会对在数据库中编程的任何方法感到失望。 当服务器要进行sorting时,就必须每次都要计算“感知”顺序。

我build议你在第一次插入数据时添加一个额外的列来存储预处理的可sortingstring,使用一些C#方法。 例如,您可以尝试将数字转换为固定宽度范围,所以“xyz1”将变成“xyz00000001”。 然后你可以使用正常的SQL Serversorting。

在冒着我自己的号angular的危险下,我写了一篇CodeProject文章,实现了CodingHorror文章中提出的问题。 随意从我的代码中窃取 。

我刚刚读了一篇关于这个话题的文章。 关键是:你只需要整数值来sorting数据,而'rec'string属于UI。 你可以在两个字段中分割信息,比如说alpha和num,按alpha和num(分开)sorting,然后显示由alpha + num组成的string。 您可以使用计算列来组成string或视图。 希望它有帮助

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

 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

只要你sorting

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

我还是不明白(可能是因为我的英文不好)。

你可以尝试:

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

但是这对于数百万条logging是无效的。

这就是为什么我build议使用触发器填充 单独的列与人的价值

此外:

  • 内置的T-SQL函数真的很慢,微软build议使用.NET函数。
  • 人的价值是不变的,所以每次查询运行时都没有意义。