문제

우리는 DB 측 페이지 매김이 있는 대규모 데이터베이스를 가지고 있습니다.이는 매우 빠른 속도로 수백만 개의 레코드에서 50개 행의 페이지를 짧은 순간에 반환합니다.

사용자는 기본적으로 정렬할 열을 선택하여 자신만의 정렬을 정의할 수 있습니다.열은 동적입니다. 일부에는 숫자 값, 일부 날짜 및 일부 텍스트가 있습니다.

대부분의 텍스트는 예상대로 정렬되지만 멍청한 방식으로 정렬됩니다.글쎄, 나는 멍청하다고 말한다. 그것은 컴퓨터에게는 의미가 있지만 사용자는 좌절감을 느낀다.

예를 들어 문자열 레코드 ID를 기준으로 정렬하면 다음과 같은 결과가 나타납니다.

rec1
rec10
rec14
rec2
rec20
rec3
rec4

...등등.

나는 이것이 숫자를 고려하기를 원합니다.

rec1
rec2
rec3
rec4
rec10
rec14
rec20

입력을 제어할 수 없으며(그렇지 않으면 앞에 000으로 형식을 지정합니다) 단일 형식에 의존할 수 없습니다. 일부는 "{알파 코드}-{부서 코드}-{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 Server는 비교가 아닌 값을 기준으로 정렬합니다. C#에서는 값을 분할하여 비교할 수 있지만 SQL에서는 일관되게 정렬되는 단일 값을 (매우 빠르게) 가져오는 논리가 필요합니다.

@moebius - 귀하의 답변은 효과가 있을 수 있지만 이러한 모든 텍스트 값에 대해 정렬 키를 추가하는 것은 추악한 타협처럼 느껴집니다.

도움이 되었습니까?

해결책

내가 본 대부분의 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 활성화" 구성 옵션을 활성화합니다., 지시를 따르다 여기 그것을 활성화합니다.그렇게 하기 전에 보안에 미치는 영향을 고려하십시오.DB 관리자가 아닌 경우 서버 구성을 변경하기 전에 관리자와 이에 대해 논의하십시오.

노트 2:이 코드는 국제화를 제대로 지원하지 않습니다(예: 소수점 표시가 "."라고 가정하고 속도에 최적화되지 않은 등).개선에 대한 제안을 환영합니다!

편집하다: 함수 이름을 다음으로 변경했습니다. 귀화하다 대신에 자연정렬, 실제 정렬을 수행하지 않기 때문입니다.

다른 팁

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 [Column]보다 성능이 훨씬 떨어지지만 OP에서 요청한 결과를 제공합니다.

편집하다:더 명확히 하기 위해, 다음과 같은 소수 값이 있는 경우 위의 내용은 작동하지 않습니다. 1, 1.15 그리고 1.5, (다음과 같이 정렬됩니다. {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 주소는 정렬되지 않습니다.

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은 이를 증명할 수 없기 때문입니다. 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');
    }
}

이 시점에서는 이것이 약간 오래된 것이라는 것을 알고 있지만 더 나은 솔루션을 검색하던 중 이 질문을 발견했습니다.현재 주문하는 기능을 사용하고 있습니다.혼합된 영숫자('항목 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)

간단한 데이터 읽기에 대한 처리 시간이 3배로 늘어나므로 완벽한 솔루션이 아닐 수도 있습니다.

다음은 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

내가 좋아하는 다른 솔루션은 다음과 같습니다.http://www.dreamchain.com/sql-and-alpha-numeric-sort-order/

Microsoft SQL은 아니지만 Postgres에 대한 솔루션을 찾다가 여기까지 왔기 때문에 여기에 추가하면 다른 사람들에게도 도움이 될 것이라고 생각했습니다.

다음을 위해 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#으로 정렬하기 위해 DB에서 데이터를 로드하는 데 문제가 있는 경우 DB에서 프로그래밍 방식으로 수행하는 모든 접근 방식에 실망하게 될 것이라고 확신합니다.서버가 정렬할 때, 매번 그랬던 것처럼 "인식된" 순서를 계산해야 합니다.

데이터가 처음 삽입될 때 일부 C# 메서드를 사용하여 전처리된 정렬 가능한 문자열을 저장할 추가 열을 추가하는 것이 좋습니다.예를 들어 숫자를 고정 너비 범위로 변환하려고 하면 "xyz1"이 "xyz00000001"로 바뀔 수 있습니다.그런 다음 일반적인 SQL Server 정렬을 사용할 수 있습니다.

나는 경적을 울릴 위험을 무릅쓰고 CodingHorror 기사에 제기된 문제를 구현하는 CodeProject 기사를 작성했습니다.자유롭게 내 코드를 훔쳐.

방금 어딘가에서 그런 주제에 관한 기사를 읽었습니다.핵심은 다음과 같습니다.데이터를 정렬하려면 정수 값만 필요하지만 'rec' 문자열은 UI에 속합니다.정보를 alpha와 num이라는 두 필드로 분할하고, alpha와 num을 기준으로(별도로) 정렬한 다음 alpha + num으로 구성된 문자열을 표시할 수 있습니다.계산된 열을 사용하여 문자열이나 뷰를 구성할 수 있습니다.도움이 되길 바랍니다

다음 코드를 사용하여 문제를 해결할 수 있습니다.

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에 감사합니다

간단히 정렬하면 됩니다.

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

 ##

나는 아직도 이해하지 못한다(아마도 내 영어가 서툴기 때문일 것이다).

다음을 시도해 볼 수 있습니다.

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

그러나 수백만 개의 레코드에는 작동하지 않습니다.

그래서 내가 트리거를 사용하도록 제안한 이유는 다음과 같습니다. 채우다 분리된인간의 가치.

게다가:

  • 내장 T-SQL 기능은 실제로 느리게 진행되며 Microsoft는 대신 .NET 기능을 사용하도록 제안합니다.
  • 인간의 가치 일정하므로 쿼리가 실행될 때마다 계산하는 점이 없습니다.
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top