2008-08-29 2 views
42

У нас есть большая база данных, на которой у нас есть разбиение на части БД. Это быстро, возвращая страницу из 50 строк из миллионов записей за небольшую долю секунды.Естественная (человеческая альфа-цифровая) сортировка в Microsoft SQL 2005

Пользователи могут определять свой собственный вид, в основном, выбирать, какой столбец сортировать. Столбцы являются динамическими - некоторые из них имеют числовые значения, некоторые даты и некоторый текст.

В то время как большинство типов, как ожидалось, текст сортируется глухим способом. Ну, я говорю глупо, это имеет смысл для компьютеров, но расстраивает пользователей.

Например, сортировка по струнной Индентификационный дает что-то вроде:

rec1 
rec10 
rec14 
rec2 
rec20 
rec3 
rec4 

... и так далее.

Я хочу, чтобы это принять во внимание числа, так:

rec1 
rec2 
rec3 
rec4 
rec10 
rec14 
rec20 

Я не могу контролировать вход (в противном случае я бы просто формат в ведущих 000s), и я не могу полагаться на один формат - некоторые из них похожи на «{alpha code} - {dept code} - {rec id}".

Я знаю несколько способов сделать это на C#, но не могу вытащить все записи для их сортировки, поскольку это было бы медленным.

Кто-нибудь знает, как быстро применить естественный вид на сервере Sql?


Мы используем:

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 - ваш ответ может работать, но он чувствует себя уродливым компромиссом, чтобы добавить ключ сортировки для всех этих текстовых значений.

+0

Этот вопрос своего рода старый, но я добавил решение CLR на основе, что я придумал, что может помочь кому-то еще ... – RedFilter 2010-01-14 13:28:33

+0

Существует а [Coding Horror статья] (HTTP : //www.codinghorror.com/blog/archives/001018.html) относительно натурального сорта. Из комментариев кажется, что эта функция недоступна в SQL Server. – 2008-08-29 16:40:02

+1

Хотя ответ @ RedFilter, а также улучшение ответа Римана Старкова на RedFilter, оба хороши, оптимальным решением для SQL Server было бы это внутренне обработать через свойство Collation. Это уже возможно в ОС, поскольку оно используется в Проводнике при сортировке файлов по имени (например, с Windows 7). Прошу проголосовать за предложение по подключению к Microsoft, чтобы эта функция была встроена в SQL Server, так что, надеюсь, на самом деле это произойдет: https://connect.microsoft.com/SQLServer/feedback/details/2932336/support-natural-sorting-digitsasnumbers-as -a-collation-option – 2017-04-25 15:09:48

ответ

26

Большинство решений на основе SQL, которые я видел, прерываются, когда данные становятся достаточно сложными (например, более одного или двух чисел в нем). Первоначально я попытался реализовать функцию NaturalSort в T-SQL, которая соответствовала моим требованиям (среди прочего, обрабатывала произвольное количество чисел в строке), но производительность была способ слишком медленным.

В конечном счете, я написал скалярную функцию CLR в C#, чтобы разрешить естественную сортировку, и даже с неоптимизированным кодом производительность, вызывающая его из SQL Server, ослеплятельно быстро. Он имеет следующие характеристики:

  • будет сортировать первые 1000 символов или так правильно (легко модифицируется в коде или в виде параметра)
  • правильно сортирует знаков после запятой, поэтому 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.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, выполните следующие команды в Query Analyzer:

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, чтобы включить его. Перед тем, как это сделать, убедитесь, что вы учитываете последствия для безопасности. Если вы не администратор db, убедитесь, что вы обсуждаете это с вашим администратором, прежде чем вносить какие-либо изменения в конфигурацию сервера.

Note2: Этот код неправильно поддерживает интернационализацию (например, предполагается, что десятичный маркер «.», Не оптимизирован для скорости и т. Д. Предложения по его улучшению приветствуются!

Edit: Переименованный функцию Naturalize вместо NaturalSort, так как он не делает каких-либо фактических сортировки.

-1

Я все еще не понимаю (возможно, из-за моего бедного английского).

Вы можете попробовать:

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

Но это не будет работать для миллионов записей.

Вот почему я предложил использовать триггер, который заполняетотдельного колонки с человеческой ценности.

Кроме того:

  • встроенные функции T-SQL действительно медленно и Microsoft предлагают использовать .NET функции вместо.
  • человеческое значение является постоянным, поэтому нет смысла рассчитывать его каждый раз при выполнении запроса.
+0

К сожалению, в T-SQL нет `human_sort`. Поэтому я считаю, что вы предлагаете функцию C#, добавленную в SQL. Кто-нибудь знает о хорошей функции для использования там? Все механизмы, которые я знаю (вкл. Джеффа из этого сообщения), сравнивают два значения, а не возвращают одно значение для сортировки условно. Кто-нибудь знает лучший, T-SQL (или даже лучший обычный SQL: 92 или 2003) способ сделать это? – Keith 2008-08-30 10:34:21

+0

См. Мой ответ - он предоставляет функцию CLR, которая возвращает скаляр, который вы можете сортировать. Он будет значительно превосходить любое решение T-SQL. – RedFilter 2010-01-14 13:29:43

1

Если у вас возникли проблемы с загрузкой данных из БД для сортировки на C#, то я уверен, что вы будете разочарованы любым подходом при программном программировании в БД. Когда сервер собирается сортировать, он должен вычислить «воспринимаемый» порядок так же, как и каждый раз.

Я бы предложил добавить дополнительный столбец для хранения предварительно обработанной сортируемой строки, используя некоторый метод C#, когда данные сначала вставлены. Вы можете попытаться преобразовать числа в диапазоны фиксированной ширины, например, так что «xyz1» превратится в «xyz00000001». Затем вы можете использовать обычную сортировку SQL Server.

Из-за опасности просчета собственного рожка я написал статью CodeProject, в которой была реализована проблема, поставленная в статье CodingHorror. Не стесняйтесь steal from my code.

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) 

Он легко утроит время обработки для простого чтения данных, поэтому это может быть не идеальное решение.

40
order by LEN(value), value 

Не идеально, но хорошо работает во многих случаях.

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 

С уважением, [email protected]

0

Я только что прочитал статью где-то о такой теме. Ключевым моментом является то, что вам нужно только целочисленное значение для сортировки данных, а строка «rec» принадлежит пользовательскому интерфейсу. Вы можете разделить информацию в двух полях, например, альфа и num, отсортировать по альфа и num (отдельно), а затем показать строку, составленную с помощью альфа + num. Вы можете использовать вычисляемый столбец для создания строки или представления. Надеюсь, это поможет

13

Я знаю, что это старый вопрос, но я только что наткнулся на него, и так как он не получил приемлемого ответа.

Я всегда использовал способы, подобные этим:

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

Единственный общий раз, что это имеет проблемы, если ваш столбец не будет приведен к VARCHAR (MAX), или если LEN ([Колонок])> 1000 (но вы можете изменить это 1000 на что-то еще, если хотите), но вы можете использовать эту приблизительную идею для того, что вам нужно.

Также это намного хуже, чем у обычного ORDER BY [Column], но это дает вам результат, заданный в OP.

Edit: Просто для дальнейшего уточнения, это выше, не будет работать, если у вас есть десятичные значения, такие как имеющие 1, 1.15 и 1.5, (они будут отсортированы в {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') 

Результат: {1, 1.15, 1.5}

и до сих пор все полностью в SQL. Это не будет сортировать IP-адреса, потому что теперь вы получаете очень конкретные комбинации чисел, а не простой текст + номер.

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 
0

Просто вы сортировать по

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

## 
2

Для следующих 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) 
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: использование символов для ясности, включая удаление дубликатов пространства в основной цикл, выходящий раз результат больше, чем предел, установив максимальную длину без подстроки и т.д. . Вот результат:

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'); 
    } 
} 
Смежные вопросы