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.15
和1.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函数。
- 人的价值是不变的,所以每次查询运行时都没有意义。